app

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

commit 57662d9fdb8ae4a5e31d1526bc79b7db3daeef55
parent aa89d5ba882454dad2131c1b7399385b63671e78
Author: triesap <tyson@radroots.org>
Date:   Mon, 20 Apr 2026 19:16:42 +0000

marketplace: add buyer orders confirmation handoff

- add the buyer orders history surface with selectable local order rows and detail view
- route checkout completion into buyer orders with the newly placed order selected
- map shared seller order states into buyer-facing localized status copy and indicators
- extend runtime, source guard, and i18n coverage for buyer order detail behavior

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 166++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/launchers/desktop/src/source_guards.rs | 21+++++++++++++++++++--
Mcrates/launchers/desktop/src/window.rs | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/shared/i18n/src/keys.rs | 16++++++++++++++++
Mcrates/shared/i18n/src/lib.rs | 22++++++++++++++++++++++
Mi18n/locales/en/messages.json | 16++++++++++++++++
6 files changed, 472 insertions(+), 51 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -6,13 +6,14 @@ use radroots_app_core::{AppDesktopRuntimePaths, AppRuntimePathsError, AppSharedA use radroots_app_models::{ ActiveSurface, AppActivityContext, AppActivityKind, AppIdentityProjection, AppStartupGate, BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection, - BuyerCheckoutDraft, BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmProfileRecord, - FarmReadiness, FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary, - FarmerSection, FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId, - OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayProjection, - PackDayScreenQueryState, PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId, - ProductsFilter, ProductsListProjection, ProductsSort, SettingsAccountProjection, - SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, + BuyerCheckoutDraft, BuyerOrderDetailProjection, BuyerProductDetailProjection, FarmId, + FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmRulesProjection, FarmSetupDraft, + FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId, + LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrdersFilter, OrdersListProjection, + OrdersScreenQueryState, PackDayProjection, PackDayScreenQueryState, PersonalSection, + PickupLocationRecord, ProductEditorDraft, ProductId, ProductsFilter, ProductsListProjection, + ProductsSort, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection, + TodayAgendaProjection, }; use radroots_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession, @@ -241,6 +242,10 @@ impl DesktopAppRuntime { self.lock_state_mut().place_personal_order() } + pub fn open_personal_order_detail(&self, order_id: OrderId) -> Result<bool, AppSqliteError> { + self.lock_state_mut().open_personal_order_detail(order_id) + } + pub fn set_personal_search_query(&self, search_query: &str) -> Result<bool, AppSqliteError> { self.lock_state_mut() .set_personal_search_query(search_query) @@ -985,6 +990,12 @@ impl DesktopAppRuntimeState { 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 personal_changed = self.mutate_personal_projection(|projection| { let mut changed = false; @@ -1000,8 +1011,8 @@ impl DesktopAppRuntimeState { projection.orders.list = refreshed_orders.clone(); changed = true; } - if projection.orders.detail.is_some() { - projection.orders.detail = None; + if projection.orders.detail.as_ref() != Some(&order_detail) { + projection.orders.detail = Some(order_detail.clone()); changed = true; } @@ -1012,6 +1023,22 @@ impl DesktopAppRuntimeState { Ok(personal_changed || section_changed) } + fn open_personal_order_detail(&mut self, order_id: OrderId) -> 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 Some(order_detail) = sqlite_store.load_buyer_order_detail(&buyer_context, order_id)? + else { + return Ok(false); + }; + + let detail_changed = self.set_personal_order_detail(Some(order_detail)); + let section_changed = self.select_personal_section(PersonalSection::Orders); + + Ok(detail_changed || section_changed) + } + fn set_personal_search_query(&mut self, search_query: &str) -> Result<bool, AppSqliteError> { let query = self.state_store.personal_projection().search.query.clone(); if query.search_query == search_query { @@ -1698,6 +1725,17 @@ impl DesktopAppRuntimeState { }) } + fn set_personal_order_detail(&mut self, detail: Option<BuyerOrderDetailProjection>) -> bool { + self.mutate_personal_projection(|projection| { + if projection.orders.detail == detail { + return false; + } + + projection.orders.detail = detail; + true + }) + } + fn replace_personal_search_query( &mut self, query: BuyerSearchScreenQueryState, @@ -3601,6 +3639,116 @@ mod tests { .storage_key(), "placed" ); + assert_eq!( + summary + .personal_projection + .orders + .detail + .as_ref() + .expect("buyer order detail should be selected") + .order_id, + summary.personal_projection.orders.list.rows[0].order_id + ); + assert_eq!( + summary + .personal_projection + .orders + .detail + .as_ref() + .expect("buyer order detail") + .order_note + .as_deref(), + Some("Leave by the cooler") + ); + } + + #[test] + fn runtime_opens_buyer_order_detail_from_personal_orders() { + let runtime = memory_runtime(); + let (account_id, farm_id) = provision_ready_farmer_account(&runtime); + assert!( + runtime + .select_active_surface(ActiveSurface::Personal) + .expect("surface should switch into marketplace") + ); + let fulfillment_window_id = seed_buyer_marketplace_support( + &runtime, + account_id.as_str(), + farm_id, + "North field farm", + "Friday pickup", + ); + let product_id = seed_product( + &runtime, + farm_id, + "Salad mix", + "Spring blend", + "published", + Some(8), + "2026-04-20T09:00:00Z", + ); + runtime + .lock_state() + .sqlite_store + .as_ref() + .expect("sqlite store") + .connection() + .execute_batch(&format!( + "update products + set availability_window_id = '{fulfillment_window_id}' + where id = '{product_id}'" + )) + .expect("buyer detail product should attach a fulfillment window"); + assert!( + runtime + .open_personal_product_detail(PersonalSection::Browse, product_id) + .expect("buyer detail should open") + ); + assert!( + runtime + .add_personal_product_to_cart(PersonalSection::Browse, false) + .expect("buyer product should add to cart") + ); + assert!( + runtime + .save_personal_checkout_draft(BuyerCheckoutDraft { + name: "Casey Buyer".to_owned(), + email: "casey@example.com".to_owned(), + phone: String::new(), + order_note: String::new(), + }) + .expect("buyer checkout draft should save") + ); + assert!( + runtime + .place_personal_order() + .expect("buyer order should place") + ); + let order_id = runtime.summary().personal_projection.orders.list.rows[0].order_id; + assert!(runtime.select_personal_section(PersonalSection::Browse)); + assert!(runtime.lock_state_mut().set_personal_order_detail(None)); + + assert!( + runtime + .open_personal_order_detail(order_id) + .expect("buyer order detail should open") + ); + + let summary = runtime.summary(); + assert_eq!( + summary.shell_projection.selected_section, + ShellSection::Personal(PersonalSection::Orders) + ); + assert_eq!( + summary + .personal_projection + .orders + .detail + .as_ref() + .expect("buyer order detail") + .order_id, + order_id + ); } #[test] diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs @@ -51,11 +51,13 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "buyer.checkout_place_failed", "buyer.checkout_save_failed", "buyer.detail_open_failed", + "buyer.order_open_failed", "bunker uri", "bunker://466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27?relay=wss%3A%2F%2Frelay.radroots.example", "buyer.fulfillment_filter_update_failed", "buyer.search_query_update_failed", "failed to add buyer product to cart", + "failed to open buyer order detail", "failed to place buyer order", "failed to remove buyer cart line", "failed to save buyer checkout draft", @@ -104,6 +106,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "buyer-nav-cart", "buyer-nav-orders", "buyer-nav-search", + "buyer-order-open", "buyer-orders-scroll", "personal-search-delivery", "personal-search-pickup", @@ -214,7 +217,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "wss://relay.radroots.example", "{currency_code} {dollars}.{cents:02}", "{} {} {}.", - "{} local orders are already available on this device.", "{quantity} {unit_label}", "{} {}", ]; @@ -276,7 +278,22 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::PersonalBrowsePlaceholderBody", "AppTextKey::PersonalSearchPlaceholderBody", "AppTextKey::PersonalCartPlaceholderBody", - "AppTextKey::PersonalOrdersPlaceholderBody", + "AppTextKey::PersonalOrdersSurfaceBody", + "AppTextKey::PersonalOrdersEmptyTitle", + "AppTextKey::PersonalOrdersEmptyBody", + "AppTextKey::PersonalOrdersListTitle", + "AppTextKey::PersonalOrdersDetailTitle", + "AppTextKey::PersonalOrdersDetailEmptyBody", + "AppTextKey::PersonalOrdersDetailFarmLabel", + "AppTextKey::PersonalOrdersDetailStatusLabel", + "AppTextKey::PersonalOrdersDetailFulfillmentLabel", + "AppTextKey::PersonalOrdersDetailNoteLabel", + "AppTextKey::PersonalOrdersDetailItemsTitle", + "AppTextKey::PersonalOrdersStatusPlaced", + "AppTextKey::PersonalOrdersStatusScheduled", + "AppTextKey::PersonalOrdersStatusReady", + "AppTextKey::PersonalOrdersStatusCompleted", + "AppTextKey::PersonalOrdersStatusRefunded", "AppTextKey::PersonalCartSurfaceBody", "AppTextKey::PersonalOrderSummaryTitle", "AppTextKey::PersonalFulfillmentTitle", diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -13,16 +13,17 @@ pub use radroots_app_models::SettingsSection as SettingsPanelViewKey; use radroots_app_models::{ AppStartupGate, BlackoutPeriodId, BlackoutPeriodRecord, BuyerCartProjection, BuyerCartReplaceConfirmationProjection, BuyerCheckoutDraft, BuyerCheckoutSummaryProjection, - BuyerListingRow, BuyerProductDetailProjection, FarmId, FarmOperatingRulesRecord, - FarmOrderMethod, FarmProfileRecord, FarmReadinessBlocker, FarmRulesProjection, - FarmRulesReadiness, FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, - FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, - LoggedOutStartupPhase, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, - OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersListRow, PackDayPackListRow, - PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState, PersonalSection, - PickupLocationId, PickupLocationRecord, ProductAttentionState, ProductEditorDraft, ProductId, - ProductListRow, ProductPricePresentation, ProductPublishBlocker, ProductStatus, ProductsFilter, - ProductsListRow, ProductsSort, ShellSection, TodayAgendaProjection, TodaySetupTaskKind, + BuyerListingRow, BuyerOrderDetailProjection, BuyerOrderStatus, BuyerOrdersListRow, + BuyerProductDetailProjection, FarmId, FarmOperatingRulesRecord, FarmOrderMethod, + FarmProfileRecord, FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness, + FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection, + FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase, + OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, + OrderStatus, OrdersFilter, OrdersListRow, PackDayPackListRow, PackDayProductTotalRow, + PackDayRosterRow, PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord, + ProductAttentionState, ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation, + ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsListRow, ProductsSort, + ShellSection, TodayAgendaProjection, TodaySetupTaskKind, }; use radroots_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome, @@ -1259,6 +1260,22 @@ impl HomeView { } } + fn open_personal_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) { + match self.runtime.open_personal_order_detail(order_id) { + Ok(true) => cx.notify(), + Ok(false) => {} + Err(runtime_error) => { + error!( + target: "buyer", + event = "buyer.order_open_failed", + error = %runtime_error, + order_id = %order_id, + "failed to open buyer order detail" + ); + } + } + } + fn select_products_filter(&mut self, filter: ProductsFilter, cx: &mut Context<Self>) { match self.runtime.select_products_filter(filter) { Ok(true) => { @@ -2067,7 +2084,9 @@ impl HomeView { PersonalSection::Cart => self .render_buyer_cart_content(runtime, cx) .into_any_element(), - PersonalSection::Orders => buyer_orders_placeholder(runtime).into_any_element(), + PersonalSection::Orders => self + .render_buyer_orders_content(runtime, cx) + .into_any_element(), }; app_split_shell( @@ -2420,6 +2439,48 @@ impl HomeView { .into_any_element() } + fn render_buyer_orders_content( + &mut self, + runtime: &DesktopAppRuntimeSummary, + cx: &mut Context<Self>, + ) -> AnyElement { + let orders = &runtime.personal_projection.orders; + let selected_order_id = orders.detail.as_ref().map(|detail| detail.order_id); + + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .max_w(px(APP_UI_THEME.shells.home_card_max_width_px)) + .mx_auto() + .child(buyer_workspace_title_block( + AppTextKey::HomeNavOrders, + AppTextKey::PersonalOrdersSurfaceBody, + )) + .child(if orders.list.rows.is_empty() { + home_empty_state_card( + AppTextKey::PersonalOrdersEmptyTitle, + AppTextKey::PersonalOrdersEmptyBody, + ) + .into_any_element() + } else { + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(buyer_orders_list_card( + &orders.list.rows, + selected_order_id, + cx, + )) + .child( + orders + .detail + .as_ref() + .map(buyer_order_detail_card) + .unwrap_or_else(|| buyer_order_detail_empty_card().into_any_element()), + ) + .into_any_element() + }) + .into_any_element() + } + fn render_farmer_workspace( &mut self, runtime: &DesktopAppRuntimeSummary, @@ -6355,38 +6416,167 @@ fn buyer_money_text(amount_minor_units: u32, currency_code: &str) -> String { } } -fn buyer_surface_placeholder( - title_key: AppTextKey, - body_key: AppTextKey, - detail: Option<String>, +fn buyer_orders_list_card( + rows: &[BuyerOrdersListRow], + selected_order_id: Option<OrderId>, + cx: &mut Context<HomeView>, ) -> AnyElement { - app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) - .w_full() - .max_w(px(APP_UI_THEME.shells.home_card_max_width_px)) - .mx_auto() - .child(app_text_value(app_shared_text(title_key))) - .child(app_surface_card( - app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) - .w_full() - .child(home_body_text(app_shared_text(body_key))) - .when_some(detail, |this, detail| this.child(home_body_text(detail))), - )) - .into_any_element() + home_card( + app_shared_text(AppTextKey::PersonalOrdersListTitle), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .children( + rows.iter() + .enumerate() + .map(|(index, row)| { + buyer_orders_list_entry( + index, + row, + selected_order_id == Some(row.order_id), + cx, + ) + }) + .collect::<Vec<_>>(), + ), + ) + .into_any_element() } -fn buyer_orders_placeholder(runtime: &DesktopAppRuntimeSummary) -> AnyElement { - let detail = (!runtime.personal_projection.orders.list.rows.is_empty()).then_some(format!( - "{} local orders are already available on this device.", - runtime.personal_projection.orders.list.rows.len() - )); +fn buyer_orders_list_entry( + index: usize, + row: &BuyerOrdersListRow, + is_selected: bool, + cx: &mut Context<HomeView>, +) -> AnyElement { + app_button_card( + ("buyer-order-open", index), + is_selected, + cx.listener({ + let order_id = row.order_id; + move |this, _, _, cx| this.open_personal_order_detail(order_id, cx) + }), + cx, + div() + .w_full() + .min_w_0() + .p(px(APP_UI_THEME.shells.home_card_padding_px)) + .flex() + .flex_col() + .gap(px(APP_UI_THEME.foundation.spacing.small_px)) + .child( + div() + .w_full() + .min_w_0() + .flex() + .items_start() + .justify_between() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child( + app_stack_v(4.0) + .flex_1() + .min_w_0() + .child(app_text_label(row.order_number.clone())) + .child(settings_badge_text(row.farm_display_name.clone())), + ) + .child( + div() + .flex() + .items_center() + .gap(px(6.0)) + .child(status_indicator(buyer_orders_status_color(row.status))) + .child( + div() + .text_size(px(APP_UI_THEME + .foundation + .typography + .utility_title_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(app_shared_text(buyer_orders_status_key(row.status))), + ), + ), + ) + .child(buyer_listing_chip(row.fulfillment_summary.clone())), + ) + .into_any_element() +} + +fn buyer_order_detail_card(detail: &BuyerOrderDetailProjection) -> AnyElement { + home_card( + app_shared_text(AppTextKey::PersonalOrdersDetailTitle), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(app_heading_section(detail.order_number.clone())) + .child(settings_badge_text(detail.farm_display_name.clone())) + .child(label_value_list([ + LabelValueRow::new( + app_shared_text(AppTextKey::PersonalOrdersDetailFarmLabel), + detail.farm_display_name.clone(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::PersonalOrdersDetailStatusLabel), + app_shared_text(buyer_orders_status_key(detail.status)), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::PersonalOrdersDetailFulfillmentLabel), + detail.fulfillment_summary.clone(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::PersonalOrdersDetailNoteLabel), + order_optional_text(detail.order_note.as_deref()), + ), + ])) + .child(app_form_section( + app_shared_text(AppTextKey::PersonalOrdersDetailItemsTitle), + div() + .w_full() + .flex() + .flex_col() + .gap(px(APP_UI_THEME.foundation.spacing.tight_px)) + .children( + detail + .items + .iter() + .map(order_detail_item_row) + .collect::<Vec<_>>(), + ) + .when(detail.items.is_empty(), |this| { + this.child(home_body_text(app_shared_text(AppTextKey::ValueNone))) + }), + )), + ) + .into_any_element() +} - buyer_surface_placeholder( - AppTextKey::HomeNavOrders, - AppTextKey::PersonalOrdersPlaceholderBody, - detail, +fn buyer_order_detail_empty_card() -> impl IntoElement { + home_card( + app_shared_text(AppTextKey::PersonalOrdersDetailTitle), + home_body_text(app_shared_text(AppTextKey::PersonalOrdersDetailEmptyBody)), ) } +fn buyer_orders_status_key(status: BuyerOrderStatus) -> AppTextKey { + match status { + BuyerOrderStatus::Placed => AppTextKey::PersonalOrdersStatusPlaced, + BuyerOrderStatus::Scheduled => AppTextKey::PersonalOrdersStatusScheduled, + BuyerOrderStatus::Ready => AppTextKey::PersonalOrdersStatusReady, + BuyerOrderStatus::Completed => AppTextKey::PersonalOrdersStatusCompleted, + BuyerOrderStatus::Refunded => AppTextKey::PersonalOrdersStatusRefunded, + } +} + +fn buyer_orders_status_color(status: BuyerOrderStatus) -> u32 { + match status { + BuyerOrderStatus::Placed => APP_UI_THEME.components.app_status_indicator.attention, + BuyerOrderStatus::Scheduled | BuyerOrderStatus::Ready => { + APP_UI_THEME.components.app_status_indicator.online + } + BuyerOrderStatus::Completed | BuyerOrderStatus::Refunded => { + APP_UI_THEME.components.app_status_indicator.offline + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum StartupHomeSurface { IssueCard, @@ -9347,8 +9537,8 @@ mod tests { AppTextKey, FarmerHomeFarmState, HomeStage, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER, SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsInventorySectionSpec, SettingsPanelViewKey, StartupHomeSurface, - StartupSignerConnectState, farm_setup_onboarding_card_spec, farmer_home_farm_state, - farmer_pack_day_available, home_content_scroll_id, home_saved_farm, + StartupSignerConnectState, buyer_orders_status_key, farm_setup_onboarding_card_spec, + farmer_home_farm_state, farmer_pack_day_available, home_content_scroll_id, home_saved_farm, home_sidebar_navigation_sections, home_stage, home_window_launch_size_px, home_window_minimum_size_px, parse_optional_product_editor_stock_input, parse_product_editor_price_input, product_display_title, startup_home_surface, @@ -9359,8 +9549,8 @@ mod tests { use crate::runtime::DesktopAppRuntimeSummary; use radroots_app_models::SettingsAccountProjection; use radroots_app_models::{ - ActiveSurface, AppStartupGate, FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, - FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId, + ActiveSurface, AppStartupGate, BuyerOrderStatus, FarmId, FarmOrderMethod, FarmReadiness, + FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId, FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection, PackDayProjection, PersonalSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, @@ -9551,6 +9741,18 @@ mod tests { } #[test] + fn buyer_orders_status_keys_use_buyer_facing_copy() { + assert_eq!( + buyer_orders_status_key(BuyerOrderStatus::Placed), + AppTextKey::PersonalOrdersStatusPlaced + ); + assert_eq!( + buyer_orders_status_key(BuyerOrderStatus::Ready), + AppTextKey::PersonalOrdersStatusReady + ); + } + + #[test] fn farmer_home_farm_state_distinguishes_no_farm_incomplete_and_configured() { let farm_id = FarmId::new(); let incomplete_farm = FarmSummary { diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -110,6 +110,22 @@ define_app_text_keys! { PersonalSearchPlaceholderBody => "personal.search.placeholder.body", PersonalCartPlaceholderBody => "personal.cart.placeholder.body", PersonalOrdersPlaceholderBody => "personal.orders.placeholder.body", + PersonalOrdersSurfaceBody => "personal.orders.surface.body", + PersonalOrdersEmptyTitle => "personal.orders.empty.title", + PersonalOrdersEmptyBody => "personal.orders.empty.body", + PersonalOrdersListTitle => "personal.orders.list.title", + PersonalOrdersDetailTitle => "personal.orders.detail.title", + PersonalOrdersDetailEmptyBody => "personal.orders.detail.empty.body", + PersonalOrdersDetailFarmLabel => "personal.orders.detail.farm.label", + PersonalOrdersDetailStatusLabel => "personal.orders.detail.status.label", + PersonalOrdersDetailFulfillmentLabel => "personal.orders.detail.fulfillment.label", + PersonalOrdersDetailNoteLabel => "personal.orders.detail.note.label", + PersonalOrdersDetailItemsTitle => "personal.orders.detail.items.title", + PersonalOrdersStatusPlaced => "personal.orders.status.placed", + PersonalOrdersStatusScheduled => "personal.orders.status.scheduled", + PersonalOrdersStatusReady => "personal.orders.status.ready", + PersonalOrdersStatusCompleted => "personal.orders.status.completed", + PersonalOrdersStatusRefunded => "personal.orders.status.refunded", PersonalCartSurfaceBody => "personal.cart.surface.body", PersonalOrderSummaryTitle => "personal.order_summary.title", PersonalFulfillmentTitle => "personal.fulfillment.title", diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs @@ -226,6 +226,28 @@ mod tests { } #[test] + fn english_marketplace_orders_copy_matches_the_buyer_history_contract() { + assert_eq!( + app_text(AppTextKey::PersonalOrdersSurfaceBody), + "Review orders placed on this device." + ); + assert_eq!( + app_text(AppTextKey::PersonalOrdersEmptyTitle), + "No orders yet" + ); + assert_eq!( + app_text(AppTextKey::PersonalOrdersListTitle), + "Order history" + ); + assert_eq!(app_text(AppTextKey::PersonalOrdersStatusPlaced), "Placed"); + assert_eq!(app_text(AppTextKey::PersonalOrdersStatusReady), "Ready"); + assert_eq!( + app_text(AppTextKey::PersonalOrdersDetailTitle), + "Order detail" + ); + } + + #[test] fn english_pack_day_copy_matches_the_contextual_execution_contract() { assert_eq!(app_text(AppTextKey::PackDayTitle), "Pack day"); assert_eq!( diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -89,6 +89,22 @@ "personal.search.placeholder.body": "Search will use the same marketplace listings and stay focused on products, farms, and pickup options.", "personal.cart.placeholder.body": "Add items from one farm to start an order.", "personal.orders.placeholder.body": "Placed orders will appear here on this device.", + "personal.orders.surface.body": "Review orders placed on this device.", + "personal.orders.empty.title": "No orders yet", + "personal.orders.empty.body": "Orders you place on this device will appear here.", + "personal.orders.list.title": "Order history", + "personal.orders.detail.title": "Order detail", + "personal.orders.detail.empty.body": "Select an order to review the details.", + "personal.orders.detail.farm.label": "Farm", + "personal.orders.detail.status.label": "Status", + "personal.orders.detail.fulfillment.label": "Fulfillment", + "personal.orders.detail.note.label": "Order note", + "personal.orders.detail.items.title": "Items", + "personal.orders.status.placed": "Placed", + "personal.orders.status.scheduled": "Scheduled", + "personal.orders.status.ready": "Ready", + "personal.orders.status.completed": "Completed", + "personal.orders.status.refunded": "Refunded", "personal.cart.surface.body": "Review items from one farm and continue to checkout when you're ready.", "personal.order_summary.title": "Order summary", "personal.fulfillment.title": "Fulfillment",