commit d474f24f7f7c357d065bea2e3acd78e70c423692
parent 05b4faf85f15cbca170b3fe7741d03b022ea2215
Author: triesap <tyson@radroots.org>
Date: Thu, 4 Jun 2026 20:28:37 -0700
orders: align lifecycle action vocabulary
Diffstat:
7 files changed, 182 insertions(+), 71 deletions(-)
diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs
@@ -99,8 +99,8 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"failed to cancel buyer order",
"failed to keep buyer order",
"failed to mark buyer order received",
- "failed to mark order delivered",
- "failed to mark order ready",
+ "failed to publish delivered order",
+ "failed to publish order ready for pickup",
"failed to open existing product editor",
"failed to open new product editor",
"failed to acknowledge reminder",
@@ -194,8 +194,8 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"pack-day-reveal-bundle",
"pack_day.export_failed",
"pack_day.route_failed",
- "orders-detail-mark-completed",
- "orders-detail-mark-packed",
+ "orders-detail-publish-delivered",
+ "orders-detail-publish-ready-for-pickup",
"orders-filter-all",
"orders-filter-completed",
"orders-filter-needs-action",
@@ -206,14 +206,14 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"orders-recovery-review",
"orders-recovery-reopen",
"orders-recovery-resolve",
- "orders-row-action-mark-completed",
- "orders-row-action-mark-packed",
+ "orders-row-action-publish-delivered",
+ "orders-row-action-publish-ready-for-pickup",
"orders-row-action-review",
"orders-row-open",
"orders.detail_open_failed",
"orders.filter_update_failed",
- "orders.mark_delivered_failed",
- "orders.ready_for_pickup_failed",
+ "orders.delivered_publish_failed",
+ "orders.ready_for_pickup_publish_failed",
"orders.recovery_reopen_failed",
"orders.recovery_resolve_failed",
"orders.recovery_review_failed",
@@ -455,7 +455,7 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::OrdersFilterAll",
"AppTextKey::OrdersStatusNeedsAction",
"AppTextKey::OrdersStatusScheduled",
- "AppTextKey::OrdersStatusPacked",
+ "AppTextKey::OrdersStatusInHandoff",
"AppTextKey::OrdersStatusCompleted",
"AppTextKey::OrdersStatusRefunded",
"AppTextKey::OrdersTableTitle",
@@ -465,8 +465,8 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::OrdersColumnPickup",
"AppTextKey::OrdersColumnAction",
"AppTextKey::OrdersActionReview",
- "AppTextKey::OrdersActionMarkPacked",
- "AppTextKey::OrdersActionMarkCompleted",
+ "AppTextKey::OrdersActionReadyForPickup",
+ "AppTextKey::OrdersActionMarkDelivered",
"AppTextKey::OrdersEmptyTitle",
"AppTextKey::OrdersEmptyBody",
"AppTextKey::OrdersEmptyNeedsActionTitle",
@@ -810,6 +810,19 @@ const FORBIDDEN_HARDCODED_WORKFLOW_UI_LITERALS: &[&str] = &[
"Unknown",
];
+const FORBIDDEN_STALE_SELLER_LIFECYCLE_WINDOW_PATTERNS: &[&str] = &[
+ concat!("orders-detail-", "mark-packed"),
+ concat!("orders-detail-", "mark-completed"),
+ concat!("orders-row-action-", "mark-packed"),
+ concat!("orders-row-action-", "mark-completed"),
+ concat!("orders.", "mark_delivered_failed"),
+ concat!("OrderPrimaryAction::", "MarkPacked"),
+ concat!("OrderPrimaryAction::", "MarkCompleted"),
+ concat!("AppTextKey::", "OrdersStatus", "Packed"),
+ concat!("AppTextKey::", "OrdersAction", "MarkPacked"),
+ concat!("AppTextKey::", "OrdersAction", "MarkCompleted"),
+];
+
const FORBIDDEN_PAYMENT_DEFERRAL_COPY_PATTERNS: &[&str] = &[
"payments are deferred",
"payment is deferred",
@@ -900,6 +913,18 @@ fn desktop_window_source_does_not_reintroduce_removed_ui_helper_families() {
}
#[test]
+fn desktop_window_source_uses_publish_lifecycle_action_identifiers() {
+ let source = include_str!("window.rs");
+
+ for pattern in FORBIDDEN_STALE_SELLER_LIFECYCLE_WINDOW_PATTERNS {
+ assert!(
+ !source.contains(pattern),
+ "window.rs still contains stale seller lifecycle action pattern `{pattern}`"
+ );
+ }
+}
+
+#[test]
fn desktop_window_source_does_not_use_about_placeholder_copy() {
let source = include_str!("window.rs");
diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs
@@ -298,8 +298,8 @@ enum HomeAutoFocusTarget {
ProductsStockInput,
ProductEditorTitleInput,
OrdersRowOpenFirst,
- OrdersDetailMarkPacked,
- OrdersDetailMarkCompleted,
+ OrdersDetailPublishReadyForPickup,
+ OrdersDetailPublishDelivered,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -477,11 +477,11 @@ impl HomeView {
HomeAutoFocusTarget::OrdersRowOpenFirst => {
focus_button(window, ("orders-row-open", 0_usize), cx);
}
- HomeAutoFocusTarget::OrdersDetailMarkPacked => {
- focus_button(window, "orders-detail-mark-packed", cx);
+ HomeAutoFocusTarget::OrdersDetailPublishReadyForPickup => {
+ focus_button(window, "orders-detail-publish-ready-for-pickup", cx);
}
- HomeAutoFocusTarget::OrdersDetailMarkCompleted => {
- focus_button(window, "orders-detail-mark-completed", cx);
+ HomeAutoFocusTarget::OrdersDetailPublishDelivered => {
+ focus_button(window, "orders-detail-publish-delivered", cx);
}
}
}
@@ -2106,10 +2106,10 @@ impl HomeView {
Err(runtime_error) => {
error!(
target: "orders",
- event = "orders.ready_for_pickup_failed",
+ event = "orders.ready_for_pickup_publish_failed",
error = %runtime_error,
order_id = %order_id,
- "failed to mark order ready"
+ "failed to publish order ready for pickup"
);
}
}
@@ -2122,10 +2122,10 @@ impl HomeView {
Err(runtime_error) => {
error!(
target: "orders",
- event = "orders.mark_delivered_failed",
+ event = "orders.delivered_publish_failed",
error = %runtime_error,
order_id = %order_id,
- "failed to mark order delivered"
+ "failed to publish delivered order"
);
}
}
@@ -3490,7 +3490,7 @@ impl HomeView {
summary.scheduled_orders,
))
.child(home_summary_metric(
- AppTextKey::OrdersStatusPacked,
+ AppTextKey::OrdersStatusInHandoff,
summary.packed_orders,
)),
)
@@ -3526,7 +3526,7 @@ impl HomeView {
))
.child(choice_button(
"orders-filter-packed",
- app_shared_text(AppTextKey::OrdersStatusPacked),
+ app_shared_text(AppTextKey::OrdersStatusInHandoff),
projection.query.filter == OrdersFilter::Packed,
cx.listener(|this, _, _, cx| {
this.select_orders_filter(OrdersFilter::Packed, cx)
@@ -4174,10 +4174,10 @@ impl HomeView {
cx: &mut Context<Self>,
) -> AnyElement {
let primary_action = match detail.primary_action {
- Some(OrderPrimaryAction::MarkPacked) => Some(
+ Some(OrderPrimaryAction::PublishReadyForPickup) => Some(
action_button_primary(
- "orders-detail-mark-packed",
- app_shared_text(AppTextKey::OrdersActionMarkPacked),
+ "orders-detail-publish-ready-for-pickup",
+ app_shared_text(AppTextKey::OrdersActionReadyForPickup),
cx.listener({
let order_id = detail.order_id;
move |this, _, _, cx| this.publish_order_ready_for_pickup(order_id, cx)
@@ -4186,10 +4186,10 @@ impl HomeView {
)
.into_any_element(),
),
- Some(OrderPrimaryAction::MarkCompleted) => Some(
+ Some(OrderPrimaryAction::PublishDelivered) => Some(
action_button_primary(
- "orders-detail-mark-completed",
- app_shared_text(AppTextKey::OrdersActionMarkCompleted),
+ "orders-detail-publish-delivered",
+ app_shared_text(AppTextKey::OrdersActionMarkDelivered),
cx.listener({
let order_id = detail.order_id;
move |this, _, _, cx| this.publish_order_delivered(order_id, cx)
@@ -7679,11 +7679,11 @@ fn farmer_auto_focus_target(
FarmerSection::Orders if farmer_products_available(runtime) => {
if let Some(detail) = runtime.orders_projection.detail.as_ref() {
match detail.primary_action {
- Some(OrderPrimaryAction::MarkPacked) => {
- Some(HomeAutoFocusTarget::OrdersDetailMarkPacked)
+ Some(OrderPrimaryAction::PublishReadyForPickup) => {
+ Some(HomeAutoFocusTarget::OrdersDetailPublishReadyForPickup)
}
- Some(OrderPrimaryAction::MarkCompleted) => {
- Some(HomeAutoFocusTarget::OrdersDetailMarkCompleted)
+ Some(OrderPrimaryAction::PublishDelivered) => {
+ Some(HomeAutoFocusTarget::OrdersDetailPublishDelivered)
}
Some(OrderPrimaryAction::Review) | None
if !runtime.orders_projection.list.rows.is_empty() =>
@@ -10395,8 +10395,8 @@ fn orders_table_action(
index: usize,
row: &OrdersListRow,
on_review: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
- on_mark_packed: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
- on_mark_completed: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_publish_ready_for_pickup: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_publish_delivered: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
cx: &App,
) -> AnyElement {
match row.primary_action {
@@ -10407,17 +10407,17 @@ fn orders_table_action(
cx,
)
.into_any_element(),
- Some(OrderPrimaryAction::MarkPacked) => action_button_compact(
- ("orders-row-action-mark-packed", index),
- app_shared_text(AppTextKey::OrdersActionMarkPacked),
- on_mark_packed,
+ Some(OrderPrimaryAction::PublishReadyForPickup) => action_button_compact(
+ ("orders-row-action-publish-ready-for-pickup", index),
+ app_shared_text(AppTextKey::OrdersActionReadyForPickup),
+ on_publish_ready_for_pickup,
cx,
)
.into_any_element(),
- Some(OrderPrimaryAction::MarkCompleted) => action_button_compact(
- ("orders-row-action-mark-completed", index),
- app_shared_text(AppTextKey::OrdersActionMarkCompleted),
- on_mark_completed,
+ Some(OrderPrimaryAction::PublishDelivered) => action_button_compact(
+ ("orders-row-action-publish-delivered", index),
+ app_shared_text(AppTextKey::OrdersActionMarkDelivered),
+ on_publish_delivered,
cx,
)
.into_any_element(),
@@ -14164,7 +14164,7 @@ mod tests {
farmer_order_id,
OrderStatus::Scheduled,
),
- primary_action: Some(OrderPrimaryAction::MarkPacked),
+ primary_action: Some(OrderPrimaryAction::PublishReadyForPickup),
}];
orders.orders_projection.detail = Some(OrderDetailProjection {
order_id: farmer_order_id,
@@ -14182,12 +14182,12 @@ mod tests {
farmer_order_id,
OrderStatus::Scheduled,
),
- primary_action: Some(OrderPrimaryAction::MarkPacked),
+ primary_action: Some(OrderPrimaryAction::PublishReadyForPickup),
recoveries: Vec::new(),
});
assert_eq!(
home_auto_focus_target(&orders, HomeAutoFocusState::default()),
- Some(HomeAutoFocusTarget::OrdersDetailMarkPacked)
+ Some(HomeAutoFocusTarget::OrdersDetailPublishReadyForPickup)
);
}
diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs
@@ -192,7 +192,7 @@ define_app_text_keys! {
OrdersFilterAll => "orders.filter.all",
OrdersStatusNeedsAction => "orders.status.needs_action",
OrdersStatusScheduled => "orders.status.scheduled",
- OrdersStatusPacked => "orders.status.packed",
+ OrdersStatusInHandoff => "orders.status.in_handoff",
OrdersStatusCompleted => "orders.status.completed",
OrdersStatusDeclined => "orders.status.declined",
OrdersStatusRefunded => "orders.status.refunded",
@@ -203,8 +203,8 @@ define_app_text_keys! {
OrdersColumnPickup => "orders.column.pickup",
OrdersColumnAction => "orders.column.action",
OrdersActionReview => "orders.action.review",
- OrdersActionMarkPacked => "orders.action.mark_packed",
- OrdersActionMarkCompleted => "orders.action.mark_completed",
+ OrdersActionReadyForPickup => "orders.action.ready_for_pickup",
+ OrdersActionMarkDelivered => "orders.action.mark_delivered",
OrdersEmptyTitle => "orders.empty.title",
OrdersEmptyBody => "orders.empty.body",
OrdersEmptyNeedsActionTitle => "orders.empty.needs_action.title",
diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs
@@ -339,9 +339,13 @@ mod tests {
"Needs action"
);
assert_eq!(app_text(AppTextKey::OrdersStatusDeclined), "Declined");
- assert_eq!(app_text(AppTextKey::OrdersActionMarkPacked), "Mark packed");
+ assert_eq!(app_text(AppTextKey::OrdersStatusInHandoff), "In handoff");
assert_eq!(
- app_text(AppTextKey::OrdersActionMarkCompleted),
+ app_text(AppTextKey::OrdersActionReadyForPickup),
+ "Ready for pickup"
+ );
+ assert_eq!(
+ app_text(AppTextKey::OrdersActionMarkDelivered),
"Mark delivered"
);
assert_eq!(app_text(AppTextKey::OrdersDetailTitle), "Order detail");
diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs
@@ -7,7 +7,7 @@ use radroots_app_view::{
PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry,
PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow,
PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow,
- PackDayScreenQueryState, ProductId, TradeWorkflowProjection,
+ PackDayScreenQueryState, ProductId, TradeFulfillmentStatus, TradeWorkflowProjection,
};
use rusqlite::{Connection, OptionalExtension, params};
@@ -173,8 +173,8 @@ impl<'a> AppOrdersRepository<'a> {
items,
economics,
payment,
+ primary_action: primary_action_for_order(status, &workflow),
workflow,
- primary_action: primary_action_for_status(status),
recoveries: Vec::new(),
})
},
@@ -1161,8 +1161,8 @@ impl OrderRecord {
fulfillment_window_label: self.fulfillment_window_label,
pickup_location_label: self.pickup_location_label,
status: self.status,
+ primary_action: primary_action_for_order(self.status, &self.workflow),
workflow: self.workflow,
- primary_action: primary_action_for_status(self.status),
}
}
}
@@ -1185,11 +1185,21 @@ fn summarize_orders(records: &[OrderRecord]) -> OrdersListSummary {
summary
}
-fn primary_action_for_status(status: OrderStatus) -> Option<OrderPrimaryAction> {
+fn primary_action_for_order(
+ status: OrderStatus,
+ workflow: &TradeWorkflowProjection,
+) -> Option<OrderPrimaryAction> {
match status {
OrderStatus::NeedsAction => Some(OrderPrimaryAction::Review),
- OrderStatus::Scheduled => Some(OrderPrimaryAction::MarkPacked),
- OrderStatus::Packed => Some(OrderPrimaryAction::MarkCompleted),
+ OrderStatus::Scheduled | OrderStatus::Packed => match workflow.fulfillment {
+ None | Some(TradeFulfillmentStatus::Confirmed | TradeFulfillmentStatus::Preparing) => {
+ Some(OrderPrimaryAction::PublishReadyForPickup)
+ }
+ Some(
+ TradeFulfillmentStatus::ReadyForPickup | TradeFulfillmentStatus::OutForDelivery,
+ ) => Some(OrderPrimaryAction::PublishDelivered),
+ Some(TradeFulfillmentStatus::Delivered | TradeFulfillmentStatus::Cancelled) => None,
+ },
OrderStatus::Completed | OrderStatus::Declined | OrderStatus::Refunded => None,
}
}
@@ -1499,7 +1509,10 @@ mod tests {
assert_eq!(detail.economics.total_minor_units, Some(1950));
assert_eq!(detail.economics.currency_code.as_deref(), Some("USD"));
assert_eq!(detail.payment, TradePaymentDisplayStatus::NotRecorded);
- assert_eq!(detail.primary_action, Some(OrderPrimaryAction::MarkPacked));
+ assert_eq!(
+ detail.primary_action,
+ Some(OrderPrimaryAction::PublishReadyForPickup)
+ );
}
#[test]
@@ -1653,6 +1666,72 @@ mod tests {
assert_eq!(workflow.economics.currency_code.as_deref(), Some("USD"));
assert_eq!(detail.payment, TradePaymentDisplayStatus::Recorded);
assert_eq!(detail.workflow, *workflow);
+ assert_eq!(
+ list.rows[0].primary_action,
+ Some(OrderPrimaryAction::PublishDelivered)
+ );
+ assert_eq!(
+ detail.primary_action,
+ Some(OrderPrimaryAction::PublishDelivered)
+ );
+ }
+
+ #[test]
+ fn seller_delivered_workflow_exposes_no_duplicate_primary_action() {
+ 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-101",
+ "Casey",
+ "packed",
+ "2026-04-17T10:00:00Z",
+ );
+ set_order_workflow_display_projection(
+ connection,
+ order_id,
+ "confirmed",
+ Some("delivered"),
+ "reserved",
+ "not_recorded",
+ "local_events",
+ Some("seller-delivered-event"),
+ );
+
+ let list = store
+ .load_orders_list(
+ farm_id,
+ &OrdersScreenQueryState {
+ filter: OrdersFilter::Packed,
+ 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");
+
+ assert_eq!(list.rows[0].status, OrderStatus::Packed);
+ assert_eq!(
+ list.rows[0].workflow.fulfillment,
+ Some(TradeFulfillmentStatus::Delivered)
+ );
+ assert_eq!(list.rows[0].primary_action, None);
+ assert_eq!(detail.primary_action, None);
}
#[test]
diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs
@@ -1648,16 +1648,16 @@ pub struct OrdersScreenQueryState {
#[serde(rename_all = "snake_case")]
pub enum OrderPrimaryAction {
Review,
- MarkPacked,
- MarkCompleted,
+ PublishReadyForPickup,
+ PublishDelivered,
}
impl OrderPrimaryAction {
pub const fn storage_key(self) -> &'static str {
match self {
Self::Review => "review",
- Self::MarkPacked => "mark_packed",
- Self::MarkCompleted => "mark_completed",
+ Self::PublishReadyForPickup => "publish_ready_for_pickup",
+ Self::PublishDelivered => "publish_delivered",
}
}
}
@@ -2825,10 +2825,13 @@ mod tests {
assert_eq!(OrdersFilter::Refunded.storage_key(), "refunded");
assert_eq!(OrderPrimaryAction::Review.storage_key(), "review");
- assert_eq!(OrderPrimaryAction::MarkPacked.storage_key(), "mark_packed");
assert_eq!(
- OrderPrimaryAction::MarkCompleted.storage_key(),
- "mark_completed"
+ OrderPrimaryAction::PublishReadyForPickup.storage_key(),
+ "publish_ready_for_pickup"
+ );
+ assert_eq!(
+ OrderPrimaryAction::PublishDelivered.storage_key(),
+ "publish_delivered"
);
}
@@ -3485,7 +3488,7 @@ mod tests {
order_id,
OrderStatus::Scheduled,
),
- primary_action: Some(OrderPrimaryAction::MarkPacked),
+ primary_action: Some(OrderPrimaryAction::PublishReadyForPickup),
}],
};
let order_detail = OrderDetailProjection {
@@ -3511,7 +3514,7 @@ mod tests {
payment: order_payment,
workflow: TradeWorkflowProjection::from_order_status(order_id, OrderStatus::Scheduled)
.with_economics_and_payment(order_economics, order_payment),
- primary_action: Some(OrderPrimaryAction::MarkPacked),
+ primary_action: Some(OrderPrimaryAction::PublishReadyForPickup),
recoveries: Vec::new(),
};
let pack_day = PackDayProjection {
@@ -3541,7 +3544,7 @@ mod tests {
assert!(!orders_list.is_empty());
assert_eq!(
orders_list.rows[0].primary_action,
- Some(OrderPrimaryAction::MarkPacked)
+ Some(OrderPrimaryAction::PublishReadyForPickup)
);
assert_eq!(
orders_list.rows[0].workflow.agreement,
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -172,7 +172,7 @@
"orders.filter.all": "All",
"orders.status.needs_action": "Needs action",
"orders.status.scheduled": "Scheduled",
- "orders.status.packed": "Packed",
+ "orders.status.in_handoff": "In handoff",
"orders.status.completed": "Completed",
"orders.status.declined": "Declined",
"orders.status.refunded": "Refunded",
@@ -183,8 +183,8 @@
"orders.column.pickup": "Pickup",
"orders.column.action": "Action",
"orders.action.review": "Review",
- "orders.action.mark_packed": "Mark packed",
- "orders.action.mark_completed": "Mark delivered",
+ "orders.action.ready_for_pickup": "Ready for pickup",
+ "orders.action.mark_delivered": "Mark delivered",
"orders.empty.title": "No orders yet",
"orders.empty.body": "Orders will appear here when customers place them.",
"orders.empty.needs_action.title": "Nothing needs action",