app

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

commit dc340809c9c216dc0f32318e6c9a9308f5c5b694
parent 806fc887e390f8d4166639421f4ed6d5db638677
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 21:53:40 -0700

app: add seller fulfillment authoring parity

Diffstat:
Mcrates/desktop/src/runtime.rs | 271++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcrates/desktop/src/source_guards.rs | 16++++++++++------
Mcrates/desktop/src/window.rs | 206++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcrates/i18n/src/keys.rs | 4++++
Mcrates/i18n/src/lib.rs | 13+++++++++++++
Mcrates/state/src/lib.rs | 2++
Mcrates/store/src/interop.rs | 35++++++++++++++++++++++++++++++++---
Mcrates/store/src/repo/orders.rs | 52++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/view/src/lib.rs | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mi18n/locales/en/messages.json | 4++++
10 files changed, 465 insertions(+), 262 deletions(-)

diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -48,8 +48,8 @@ use radroots_app_view::{ BuyerContext, BuyerOrderDetailProjection, BuyerOrderReviewDraft, BuyerOrderStatus, BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, - FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId, - OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListProjection, + FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderFulfillmentAction, + OrderId, OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayBatchPrintStatus, PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintKind, PackDayPrintStatus, PackDayProjection, PackDayScreenQueryState, PersonalSection, @@ -805,21 +805,13 @@ impl DesktopAppRuntime { ) } - pub fn publish_order_ready_for_pickup( + pub fn publish_order_fulfillment_update( &self, order_id: OrderId, + action: OrderFulfillmentAction, ) -> Result<bool, AppSqliteError> { - self.lock_state_mut().publish_seller_order_fulfillment( - order_id, - RadrootsActiveTradeFulfillmentState::ReadyForPickup, - ) - } - - pub fn publish_order_delivered(&self, order_id: OrderId) -> Result<bool, AppSqliteError> { - self.lock_state_mut().publish_seller_order_fulfillment( - order_id, - RadrootsActiveTradeFulfillmentState::Delivered, - ) + self.lock_state_mut() + .publish_seller_order_fulfillment(order_id, action.fulfillment_status()) } pub fn publish_order_revision_proposal( @@ -2611,57 +2603,17 @@ impl DesktopAppRuntimeState { reason: "seller order fulfillment requires a visible seller order", }); }; - let latest_fulfillment = lifecycle.latest_fulfillment.as_ref(); - let prev_event_id = match status { - RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled - | RadrootsActiveTradeFulfillmentState::Preparing - | RadrootsActiveTradeFulfillmentState::OutForDelivery => { - return Err(AppSqliteError::InvalidProjection { - reason: "seller order fulfillment status must be publishable", - }); - } - RadrootsActiveTradeFulfillmentState::ReadyForPickup => match latest_fulfillment { - None => active_order_current_parent_event_id( - &lifecycle, - "seller order fulfillment requires current lifecycle parent evidence", - )?, - Some(fulfillment) - if matches!( - fulfillment.status, - RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled - | RadrootsActiveTradeFulfillmentState::Preparing - ) => - { - fulfillment.event_id.clone() - } - Some(_) => { - return Err(AppSqliteError::InvalidProjection { - reason: "seller order ready for pickup requires accepted or preparing fulfillment evidence", - }); - } - }, - RadrootsActiveTradeFulfillmentState::Delivered => match latest_fulfillment { - Some(fulfillment) - if matches!( - fulfillment.status, - RadrootsActiveTradeFulfillmentState::ReadyForPickup - | RadrootsActiveTradeFulfillmentState::OutForDelivery - ) => - { - fulfillment.event_id.clone() - } - Some(_) | None => { - return Err(AppSqliteError::InvalidProjection { - reason: "seller order delivery requires ready fulfillment evidence", - }); - } - }, - RadrootsActiveTradeFulfillmentState::SellerCancelled => { - active_order_current_parent_event_id( - &lifecycle, - "seller order fulfillment requires current lifecycle parent evidence", - )? - } + if !status.is_publishable_update() { + return Err(AppSqliteError::InvalidProjection { + reason: "seller order fulfillment status must be publishable", + }); + } + let prev_event_id = match lifecycle.latest_fulfillment.as_ref() { + Some(fulfillment) => fulfillment.event_id.clone(), + None => active_order_current_parent_event_id( + &lifecycle, + "seller order fulfillment requires current lifecycle parent evidence", + )?, }; let payload = AppOrderFulfillmentPublishPayload { context: AppPublishContext::new(account_id, "seller_order_fulfillment"), @@ -9786,10 +9738,10 @@ mod tests { BuyerOrderStatus, FarmId, FarmOperatingRulesRecord, FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmReadinessBlocker, FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerActivationProjection, FarmerSection, - FulfillmentWindowId, FulfillmentWindowRecord, LoggedOutStartupProjection, OrderId, - OrderStatus, OrdersFilter, PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, - PackDayBatchPrintStatus, PackDayExportInstanceId, PackDayExportStatus, - PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, + FulfillmentWindowId, FulfillmentWindowRecord, LoggedOutStartupProjection, + OrderFulfillmentAction, OrderId, OrderStatus, OrdersFilter, PackDayBatchPrintArtifact, + PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, PackDayExportInstanceId, + PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductStatus, @@ -15069,61 +15021,80 @@ mod tests { } #[test] - fn runtime_publishes_seller_fulfillment_updates_and_projects_signed_evidence() { - let relay = ThreadedAckRelay::spawn(); - let (runtime, paths, order_id, product_id, seller_pubkey, buyer_pubkey) = - seller_order_decision_runtime("seller_order_fulfillment_publish", 6, 2); - install_direct_relay_sync_transport(&runtime, &relay); - publish_prior_relay_seller_order_accept( - &runtime, - &relay, - order_id, - product_id, - seller_pubkey.as_str(), - buyer_pubkey.as_str(), - ); - - assert!( - runtime - .publish_order_ready_for_pickup(order_id) - .expect("seller order ready update should publish") - ); - - assert_eq!(persisted_order_status(&runtime, order_id), "packed"); - assert_eq!(relay.event_count(), 2); - let fulfillment_events = shared_order_events_by_kind(&paths, 3433, seller_pubkey.as_str()); - assert_eq!(fulfillment_events.len(), 1); - let ready_event = fulfillment_events.first().expect("ready event"); - let ready_envelope = radroots_sdk::trade::parse_fulfillment_update(ready_event) - .expect("ready fulfillment should parse"); - assert_eq!( - ready_envelope.payload.status, - RadrootsActiveTradeFulfillmentState::ReadyForPickup - ); - assert!(event_has_tag( - ready_event, - "e_root", - "event-app:signed_event:order-request:seller-order-decision-1" - )); + fn runtime_publishes_all_seller_fulfillment_states_and_projects_signed_evidence() { + for (label, action, expected_status, expected_order_status) in [ + ( + "preparing", + OrderFulfillmentAction::Preparing, + RadrootsActiveTradeFulfillmentState::Preparing, + "scheduled", + ), + ( + "ready_for_pickup", + OrderFulfillmentAction::ReadyForPickup, + RadrootsActiveTradeFulfillmentState::ReadyForPickup, + "packed", + ), + ( + "out_for_delivery", + OrderFulfillmentAction::OutForDelivery, + RadrootsActiveTradeFulfillmentState::OutForDelivery, + "packed", + ), + ( + "delivered", + OrderFulfillmentAction::Delivered, + RadrootsActiveTradeFulfillmentState::Delivered, + "packed", + ), + ( + "seller_cancelled", + OrderFulfillmentAction::SellerCancelled, + RadrootsActiveTradeFulfillmentState::SellerCancelled, + "declined", + ), + ] { + let relay = ThreadedAckRelay::spawn(); + let runtime_label = format!("seller_order_fulfillment_publish_{label}"); + let (runtime, paths, order_id, product_id, seller_pubkey, buyer_pubkey) = + seller_order_decision_runtime(runtime_label.as_str(), 6, 2); + install_direct_relay_sync_transport(&runtime, &relay); + publish_prior_relay_seller_order_accept( + &runtime, + &relay, + order_id, + product_id, + seller_pubkey.as_str(), + buyer_pubkey.as_str(), + ); - assert!( - runtime - .publish_order_delivered(order_id) - .expect("seller order delivered update should publish") - ); + assert!( + runtime + .publish_order_fulfillment_update(order_id, action) + .expect("seller fulfillment update should publish") + ); - assert_eq!(relay.event_count(), 3); - let fulfillment_events = shared_order_events_by_kind(&paths, 3433, seller_pubkey.as_str()); - assert_eq!(fulfillment_events.len(), 2); - assert!(fulfillment_events.iter().any(|event| { - radroots_sdk::trade::parse_fulfillment_update(event) - .map(|envelope| { - envelope.payload.status == RadrootsActiveTradeFulfillmentState::Delivered - }) - .unwrap_or(false) - })); + assert_eq!( + persisted_order_status(&runtime, order_id), + expected_order_status + ); + assert_eq!(relay.event_count(), 2); + let fulfillment_events = + shared_order_events_by_kind(&paths, 3433, seller_pubkey.as_str()); + assert_eq!(fulfillment_events.len(), 1); + let fulfillment_event = fulfillment_events.first().expect("fulfillment event"); + let envelope = radroots_sdk::trade::parse_fulfillment_update(fulfillment_event) + .expect("fulfillment should parse"); + assert_eq!(envelope.payload.status, expected_status); + assert!(event_has_tag( + fulfillment_event, + "e_root", + "event-app:signed_event:order-request:seller-order-decision-1" + )); + assert!(event_has_nonempty_value_tag(fulfillment_event, "e_prev")); - cleanup_bootstrapped_runtime_paths(&paths); + cleanup_bootstrapped_runtime_paths(&paths); + } } #[test] @@ -15185,7 +15156,10 @@ mod tests { assert!( runtime - .publish_order_ready_for_pickup(order_id) + .publish_order_fulfillment_update( + order_id, + OrderFulfillmentAction::ReadyForPickup, + ) .expect("seller ready fulfillment should publish from revision parent") ); @@ -15240,7 +15214,7 @@ mod tests { assert!( runtime - .publish_order_delivered(order_id) + .publish_order_fulfillment_update(order_id, OrderFulfillmentAction::Delivered) .expect("seller delivered fulfillment should publish from workflow evidence") ); @@ -15268,7 +15242,7 @@ mod tests { } #[test] - fn runtime_rejects_seller_order_fulfillment_delivered_without_ready_evidence() { + fn runtime_publishes_seller_order_fulfillment_delivered_without_ready_evidence() { for (label, latest_fulfillment) in [ ("seller_order_fulfillment_delivery_missing_ready", None), ( @@ -15308,17 +15282,37 @@ mod tests { .refresh_shared_local_events() .expect("seller fulfillment fixture should import"); - let error = runtime - .publish_order_delivered(order_id) - .expect_err("seller delivered fulfillment should require ready evidence"); + assert!( + runtime + .publish_order_fulfillment_update(order_id, OrderFulfillmentAction::Delivered) + .expect("seller delivered fulfillment should publish") + ); - assert!(matches!( - error, - AppSqliteError::InvalidProjection { - reason: "seller order delivery requires ready fulfillment evidence" - } + assert_eq!(persisted_order_status(&runtime, order_id), "packed"); + assert_eq!(relay.event_count(), 1); + let fulfillment_events = + shared_order_events_by_kind(&paths, 3433, seller_pubkey.as_str()); + let delivered_event = fulfillment_events + .iter() + .find(|event| { + radroots_sdk::trade::parse_fulfillment_update(event) + .map(|envelope| { + envelope.payload.status + == RadrootsActiveTradeFulfillmentState::Delivered + }) + .unwrap_or(false) + }) + .expect("delivered fulfillment event should exist"); + assert!(event_has_tag( + delivered_event, + "e_prev", + latest_fulfillment + .map(|_| { + "event-app:signed_event:fulfillment:seller-order-decision-1".to_owned() + }) + .unwrap_or_else(|| decision_event_id.clone()) + .as_str() )); - assert_eq!(relay.event_count(), 0); cleanup_bootstrapped_runtime_paths(&paths); } } @@ -15383,7 +15377,7 @@ mod tests { } let error = runtime - .publish_order_delivered(order_id) + .publish_order_fulfillment_update(order_id, OrderFulfillmentAction::Delivered) .expect_err("seller delivered fulfillment should reject reducer-invalid evidence"); assert_order_lifecycle_evidence_invalid(error); @@ -15440,7 +15434,7 @@ mod tests { } let error = runtime - .publish_order_ready_for_pickup(order_id) + .publish_order_fulfillment_update(order_id, OrderFulfillmentAction::ReadyForPickup) .expect_err("seller ready fulfillment should reject invalid terminal evidence"); assert_order_lifecycle_evidence_invalid(error); @@ -21627,6 +21621,13 @@ mod tests { }) } + fn event_has_nonempty_value_tag(event: &radroots_sdk::RadrootsNostrEvent, key: &str) -> bool { + event.tags.iter().any(|tag| { + tag.first().map(String::as_str) == Some(key) + && tag.get(1).map(|value| !value.is_empty()).unwrap_or(false) + }) + } + fn persisted_order_status(runtime: &DesktopAppRuntime, order_id: OrderId) -> String { runtime .lock_state() diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -99,8 +99,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "failed to cancel buyer order", "failed to keep buyer order", "failed to mark buyer order received", - "failed to publish delivered order", - "failed to publish order ready for pickup", + "failed to publish order fulfillment update", "failed to open existing product editor", "failed to open new product editor", "failed to acknowledge reminder", @@ -195,7 +194,10 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "pack_day.export_failed", "pack_day.route_failed", "orders-detail-publish-delivered", + "orders-detail-publish-out-for-delivery", + "orders-detail-publish-preparing", "orders-detail-publish-ready-for-pickup", + "orders-detail-publish-seller-cancelled", "orders-filter-all", "orders-filter-completed", "orders-filter-needs-action", @@ -206,14 +208,12 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "orders-recovery-review", "orders-recovery-reopen", "orders-recovery-resolve", - "orders-row-action-publish-delivered", - "orders-row-action-publish-ready-for-pickup", + "orders-row-action-publish-fulfillment", "orders-row-action-review", "orders-row-open", "orders.detail_open_failed", "orders.filter_update_failed", - "orders.delivered_publish_failed", - "orders.ready_for_pickup_publish_failed", + "orders.fulfillment_publish_failed", "orders.recovery_reopen_failed", "orders.recovery_resolve_failed", "orders.recovery_review_failed", @@ -465,8 +465,12 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::OrdersColumnPickup", "AppTextKey::OrdersColumnAction", "AppTextKey::OrdersActionReview", + "AppTextKey::OrdersActionPreparing", "AppTextKey::OrdersActionReadyForPickup", + "AppTextKey::OrdersActionOutForDelivery", "AppTextKey::OrdersActionMarkDelivered", + "AppTextKey::OrdersActionCancelFulfillment", + "AppTextKey::OrdersActionUpdateFulfillment", "AppTextKey::OrdersEmptyTitle", "AppTextKey::OrdersEmptyBody", "AppTextKey::OrdersEmptyNeedsActionTitle", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -54,8 +54,8 @@ use radroots_app_view::{ FarmProfileRecord, FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness, FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase, - OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, - OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, + OrderDetailItemRow, OrderDetailProjection, OrderFulfillmentAction, OrderId, OrderListRow, + OrderPrimaryAction, OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, PackDayExportBundle, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, @@ -298,8 +298,7 @@ enum HomeAutoFocusTarget { ProductsStockInput, ProductEditorTitleInput, OrdersRowOpenFirst, - OrdersDetailPublishReadyForPickup, - OrdersDetailPublishDelivered, + OrdersDetailPublishFulfillmentFirst, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -477,11 +476,8 @@ impl HomeView { HomeAutoFocusTarget::OrdersRowOpenFirst => { focus_button(window, ("orders-row-open", 0_usize), cx); } - HomeAutoFocusTarget::OrdersDetailPublishReadyForPickup => { - focus_button(window, "orders-detail-publish-ready-for-pickup", cx); - } - HomeAutoFocusTarget::OrdersDetailPublishDelivered => { - focus_button(window, "orders-detail-publish-delivered", cx); + HomeAutoFocusTarget::OrdersDetailPublishFulfillmentFirst => { + focus_button(window, "orders-detail-publish-preparing", cx); } } } @@ -2099,33 +2095,26 @@ impl HomeView { } } - fn publish_order_ready_for_pickup(&mut self, order_id: OrderId, cx: &mut Context<Self>) { - match self.runtime.publish_order_ready_for_pickup(order_id) { - Ok(true) => cx.notify(), - Ok(false) => {} - Err(runtime_error) => { - error!( - target: "orders", - event = "orders.ready_for_pickup_publish_failed", - error = %runtime_error, - order_id = %order_id, - "failed to publish order ready for pickup" - ); - } - } - } - - fn publish_order_delivered(&mut self, order_id: OrderId, cx: &mut Context<Self>) { - match self.runtime.publish_order_delivered(order_id) { + fn publish_order_fulfillment_update( + &mut self, + order_id: OrderId, + action: OrderFulfillmentAction, + cx: &mut Context<Self>, + ) { + match self + .runtime + .publish_order_fulfillment_update(order_id, action) + { Ok(true) => cx.notify(), Ok(false) => {} Err(runtime_error) => { error!( target: "orders", - event = "orders.delivered_publish_failed", + event = "orders.fulfillment_publish_failed", error = %runtime_error, order_id = %order_id, - "failed to publish delivered order" + fulfillment_state = action.storage_key(), + "failed to publish order fulfillment update" ); } } @@ -4173,33 +4162,32 @@ impl HomeView { detail: &OrderDetailProjection, cx: &mut Context<Self>, ) -> AnyElement { - let primary_action = match detail.primary_action { - Some(OrderPrimaryAction::PublishReadyForPickup) => Some( - action_button_primary( - "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) - }), - cx, - ) - .into_any_element(), - ), - Some(OrderPrimaryAction::PublishDelivered) => Some( - action_button_primary( - "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) - }), - cx, - ) - .into_any_element(), - ), - Some(OrderPrimaryAction::Review) | None => None, - }; + let fulfillment_actions = (!detail.fulfillment_actions.is_empty()).then(|| { + app_form_section( + app_shared_text(AppTextKey::TradeWorkflowAxisFulfillment), + app_cluster(APP_UI_THEME.foundation.spacing.tight_px).children( + detail + .fulfillment_actions + .iter() + .copied() + .map(|action| { + action_button_compact( + order_detail_fulfillment_action_id(action), + app_shared_text(order_fulfillment_action_label_key(action)), + cx.listener({ + let order_id = detail.order_id; + move |this, _, _, cx| { + this.publish_order_fulfillment_update(order_id, action, cx) + } + }), + cx, + ) + .into_any_element() + }) + .collect::<Vec<_>>(), + ), + ) + }); home_card( app_shared_text(AppTextKey::OrdersDetailTitle), @@ -4244,8 +4232,8 @@ impl HomeView { }), )) .child(self.render_order_recovery_section(detail, cx)) - .when_some(primary_action, |this, primary_action| { - this.child(div().child(primary_action)) + .when_some(fulfillment_actions, |this, fulfillment_actions| { + this.child(fulfillment_actions) }), ) .into_any_element() @@ -4555,11 +4543,14 @@ impl HomeView { }), cx.listener({ let order_id = row.order_id; - move |this, _, _, cx| this.publish_order_ready_for_pickup(order_id, cx) - }), - cx.listener({ - let order_id = row.order_id; - move |this, _, _, cx| this.publish_order_delivered(order_id, cx) + let action = row + .primary_action + .and_then(OrderPrimaryAction::fulfillment_action); + move |this, _, _, cx| { + if let Some(action) = action { + this.publish_order_fulfillment_update(order_id, action, cx); + } + } }), cx, ); @@ -7678,19 +7669,12 @@ 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::PublishReadyForPickup) => { - Some(HomeAutoFocusTarget::OrdersDetailPublishReadyForPickup) - } - Some(OrderPrimaryAction::PublishDelivered) => { - Some(HomeAutoFocusTarget::OrdersDetailPublishDelivered) - } - Some(OrderPrimaryAction::Review) | None - if !runtime.orders_projection.list.rows.is_empty() => - { - Some(HomeAutoFocusTarget::OrdersRowOpenFirst) - } - Some(OrderPrimaryAction::Review) | None => None, + if !detail.fulfillment_actions.is_empty() { + Some(HomeAutoFocusTarget::OrdersDetailPublishFulfillmentFirst) + } else if !runtime.orders_projection.list.rows.is_empty() { + Some(HomeAutoFocusTarget::OrdersRowOpenFirst) + } else { + None } } else if !runtime.orders_projection.list.rows.is_empty() { Some(HomeAutoFocusTarget::OrdersRowOpenFirst) @@ -10397,8 +10381,7 @@ fn orders_table_action( index: usize, row: &OrdersListRow, on_review: 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, + on_publish_fulfillment: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> AnyElement { match row.primary_action { @@ -10409,17 +10392,16 @@ fn orders_table_action( cx, ) .into_any_element(), - 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::PublishDelivered) => action_button_compact( - ("orders-row-action-publish-delivered", index), - app_shared_text(AppTextKey::OrdersActionMarkDelivered), - on_publish_delivered, + Some( + OrderPrimaryAction::PublishPreparing + | OrderPrimaryAction::PublishReadyForPickup + | OrderPrimaryAction::PublishOutForDelivery + | OrderPrimaryAction::PublishDelivered + | OrderPrimaryAction::PublishSellerCancelled, + ) => action_button_compact( + ("orders-row-action-publish-fulfillment", index), + app_shared_text(AppTextKey::OrdersActionUpdateFulfillment), + on_publish_fulfillment, cx, ) .into_any_element(), @@ -10431,6 +10413,26 @@ fn orders_table_action( } } +fn order_detail_fulfillment_action_id(action: OrderFulfillmentAction) -> &'static str { + match action { + OrderFulfillmentAction::Preparing => "orders-detail-publish-preparing", + OrderFulfillmentAction::ReadyForPickup => "orders-detail-publish-ready-for-pickup", + OrderFulfillmentAction::OutForDelivery => "orders-detail-publish-out-for-delivery", + OrderFulfillmentAction::Delivered => "orders-detail-publish-delivered", + OrderFulfillmentAction::SellerCancelled => "orders-detail-publish-seller-cancelled", + } +} + +fn order_fulfillment_action_label_key(action: OrderFulfillmentAction) -> AppTextKey { + match action { + OrderFulfillmentAction::Preparing => AppTextKey::OrdersActionPreparing, + OrderFulfillmentAction::ReadyForPickup => AppTextKey::OrdersActionReadyForPickup, + OrderFulfillmentAction::OutForDelivery => AppTextKey::OrdersActionOutForDelivery, + OrderFulfillmentAction::Delivered => AppTextKey::OrdersActionMarkDelivered, + OrderFulfillmentAction::SellerCancelled => AppTextKey::OrdersActionCancelFulfillment, + } +} + fn orders_empty_state_card(filter: OrdersFilter) -> impl IntoElement { let (title_key, body_key) = if filter == OrdersFilter::NeedsAction { ( @@ -13400,15 +13402,15 @@ mod tests { BuyerOrdersListRow, FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId, FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection, - OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersListRow, - PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, PackDayExportArtifact, - PackDayExportArtifactKind, PackDayExportBundle, PackDayHostHandoffKind, - PackDayHostHandoffStatus, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, - PackDayProductTotalRow, PackDayProjection, PersonalSection, ProductId, - ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, ReminderKind, - ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection, - ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, - TradeAgreementStatus, TradeEconomicsProjection, TradeFulfillmentStatus, + OrderDetailProjection, OrderFulfillmentAction, OrderId, OrderPrimaryAction, OrderStatus, + OrdersListRow, PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, + PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, + PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintFailureKind, + PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, + PersonalSection, ProductId, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, + ReminderKind, ReminderSurface, ReminderUrgency, RepeatDemandEligibility, + RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, TodaySetupTask, + TodaySetupTaskKind, TradeAgreementStatus, TradeEconomicsProjection, TradeFulfillmentStatus, TradeInventoryStatus, TradePaymentDisplayStatus, TradeRevisionStatus, TradeWorkflowProjection, TradeWorkflowSource, }; @@ -14203,7 +14205,8 @@ mod tests { farmer_order_id, OrderStatus::Scheduled, ), - primary_action: Some(OrderPrimaryAction::PublishReadyForPickup), + primary_action: Some(OrderPrimaryAction::PublishPreparing), + fulfillment_actions: OrderFulfillmentAction::ALL.to_vec(), }]; orders.orders_projection.detail = Some(OrderDetailProjection { order_id: farmer_order_id, @@ -14221,12 +14224,13 @@ mod tests { farmer_order_id, OrderStatus::Scheduled, ), - primary_action: Some(OrderPrimaryAction::PublishReadyForPickup), + primary_action: Some(OrderPrimaryAction::PublishPreparing), + fulfillment_actions: OrderFulfillmentAction::ALL.to_vec(), recoveries: Vec::new(), }); assert_eq!( home_auto_focus_target(&orders, HomeAutoFocusState::default()), - Some(HomeAutoFocusTarget::OrdersDetailPublishReadyForPickup) + Some(HomeAutoFocusTarget::OrdersDetailPublishFulfillmentFirst) ); } diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -203,8 +203,12 @@ define_app_text_keys! { OrdersColumnPickup => "orders.column.pickup", OrdersColumnAction => "orders.column.action", OrdersActionReview => "orders.action.review", + OrdersActionPreparing => "orders.action.preparing", OrdersActionReadyForPickup => "orders.action.ready_for_pickup", + OrdersActionOutForDelivery => "orders.action.out_for_delivery", OrdersActionMarkDelivered => "orders.action.mark_delivered", + OrdersActionCancelFulfillment => "orders.action.cancel_fulfillment", + OrdersActionUpdateFulfillment => "orders.action.update_fulfillment", 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 @@ -344,10 +344,23 @@ mod tests { app_text(AppTextKey::OrdersActionReadyForPickup), "Ready for pickup" ); + assert_eq!(app_text(AppTextKey::OrdersActionPreparing), "Preparing"); + assert_eq!( + app_text(AppTextKey::OrdersActionOutForDelivery), + "Out for delivery" + ); assert_eq!( app_text(AppTextKey::OrdersActionMarkDelivered), "Mark delivered" ); + assert_eq!( + app_text(AppTextKey::OrdersActionCancelFulfillment), + "Cancel fulfillment" + ); + assert_eq!( + app_text(AppTextKey::OrdersActionUpdateFulfillment), + "Update" + ); assert_eq!(app_text(AppTextKey::OrdersDetailTitle), "Order detail"); assert_eq!(app_text(AppTextKey::OrdersRecoverySectionTitle), "Recovery"); assert_eq!( diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs @@ -2529,6 +2529,7 @@ mod tests { OrderStatus::NeedsAction, ), primary_action: Some(OrderPrimaryAction::Review), + fulfillment_actions: Vec::new(), }], }; let order_detail = OrderDetailProjection { @@ -2558,6 +2559,7 @@ mod tests { ) .with_economics_and_payment(order_economics, order_payment), primary_action: Some(OrderPrimaryAction::Review), + fulfillment_actions: Vec::new(), recoveries: Vec::new(), }; let orders_reminders = ReminderFeedProjection { diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs @@ -3633,9 +3633,10 @@ mod tests { use std::collections::BTreeSet; use radroots_app_view::{ - BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderStatus, OrdersFilter, - OrdersScreenQueryState, ProductAvailabilityState, ProductId, TradePaymentDisplayStatus, - TradeRevisionStatus, TradeWorkflowSource, + BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderFulfillmentAction, + OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersScreenQueryState, + ProductAvailabilityState, ProductId, TradeFulfillmentStatus, TradeInventoryStatus, + TradePaymentDisplayStatus, TradeRevisionStatus, TradeWorkflowSource, }; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, @@ -5184,6 +5185,14 @@ mod tests { ) .expect("load lifecycle seller orders after decision"); assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled); + assert_eq!( + seller_orders.rows[0].primary_action, + Some(OrderPrimaryAction::PublishPreparing) + ); + assert_eq!( + seller_orders.rows[0].fulfillment_actions, + OrderFulfillmentAction::ALL.to_vec() + ); let fulfillment_payload = fulfillment_update_payload( order_id_raw, @@ -5229,6 +5238,22 @@ mod tests { .expect("load lifecycle seller orders after fulfillment"); assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Ready); assert_eq!(seller_orders.rows[0].status, OrderStatus::Packed); + assert_eq!( + seller_orders.rows[0].workflow.fulfillment, + Some(TradeFulfillmentStatus::ReadyForPickup) + ); + assert_eq!( + seller_orders.rows[0].workflow.inventory, + TradeInventoryStatus::Reserved + ); + assert_eq!( + seller_orders.rows[0].primary_action, + Some(OrderPrimaryAction::PublishDelivered) + ); + assert_eq!( + seller_orders.rows[0].fulfillment_actions, + OrderFulfillmentAction::ALL.to_vec() + ); let receipt_payload = buyer_receipt_payload( order_id_raw, @@ -5275,6 +5300,8 @@ mod tests { .expect("load lifecycle seller orders after receipt"); assert_eq!(buyer_detail.status, BuyerOrderStatus::Completed); assert_eq!(seller_orders.rows[0].status, OrderStatus::Completed); + assert_eq!(seller_orders.rows[0].primary_action, None); + assert_eq!(seller_orders.rows[0].fulfillment_actions, Vec::new()); } #[test] @@ -5530,6 +5557,8 @@ mod tests { assert_eq!(buyer_detail.status, BuyerOrderStatus::Declined); assert_eq!(buyer_detail.workflow.revision, TradeRevisionStatus::Updated); assert_eq!(seller_orders.rows[0].status, OrderStatus::Declined); + assert_eq!(seller_orders.rows[0].primary_action, None); + assert_eq!(seller_orders.rows[0].fulfillment_actions, Vec::new()); } #[test] diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs @@ -2,8 +2,8 @@ use std::collections::BTreeMap; use radroots_app_view::{ FarmId, FulfillmentWindowId, FulfillmentWindowSummary, OrderDetailItemRow, - OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter, - OrdersListProjection, OrdersListRow, OrdersListSummary, OrdersScreenQueryState, + OrderDetailProjection, OrderFulfillmentAction, OrderId, OrderPrimaryAction, OrderStatus, + OrdersFilter, OrdersListProjection, OrdersListRow, OrdersListSummary, OrdersScreenQueryState, PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow, PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, @@ -174,6 +174,7 @@ impl<'a> AppOrdersRepository<'a> { economics, payment, primary_action: primary_action_for_order(status, &workflow), + fulfillment_actions: fulfillment_actions_for_order(status, &workflow), workflow, recoveries: Vec::new(), }) @@ -1162,6 +1163,7 @@ impl OrderRecord { pickup_location_label: self.pickup_location_label, status: self.status, primary_action: primary_action_for_order(self.status, &self.workflow), + fulfillment_actions: fulfillment_actions_for_order(self.status, &self.workflow), workflow: self.workflow, } } @@ -1193,7 +1195,7 @@ fn primary_action_for_order( OrderStatus::NeedsAction => Some(OrderPrimaryAction::Review), OrderStatus::Scheduled | OrderStatus::Packed => match workflow.fulfillment { None | Some(TradeFulfillmentStatus::Confirmed | TradeFulfillmentStatus::Preparing) => { - Some(OrderPrimaryAction::PublishReadyForPickup) + Some(OrderPrimaryAction::PublishPreparing) } Some( TradeFulfillmentStatus::ReadyForPickup | TradeFulfillmentStatus::OutForDelivery, @@ -1204,6 +1206,25 @@ fn primary_action_for_order( } } +fn fulfillment_actions_for_order( + status: OrderStatus, + workflow: &TradeWorkflowProjection, +) -> Vec<OrderFulfillmentAction> { + match (status, workflow.fulfillment) { + ( + OrderStatus::Scheduled | OrderStatus::Packed, + None + | Some( + TradeFulfillmentStatus::Confirmed + | TradeFulfillmentStatus::Preparing + | TradeFulfillmentStatus::ReadyForPickup + | TradeFulfillmentStatus::OutForDelivery, + ), + ) => OrderFulfillmentAction::ALL.to_vec(), + _ => Vec::new(), + } +} + fn format_quantity_display(quantity_value: u32, quantity_unit_label: &str) -> String { if quantity_unit_label.trim().is_empty() { quantity_value.to_string() @@ -1287,10 +1308,11 @@ fn empty_string_to_none(value: Option<String>) -> Option<String> { #[cfg(test)] mod tests { use radroots_app_view::{ - FarmId, FulfillmentWindowId, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter, - OrdersScreenQueryState, PackDayOutputOrderState, PackDayProductTotalRow, - PackDayScreenQueryState, PickupLocationId, TradeAgreementStatus, TradeFulfillmentStatus, - TradeInventoryStatus, TradePaymentDisplayStatus, TradeRevisionStatus, TradeWorkflowSource, + FarmId, FulfillmentWindowId, OrderFulfillmentAction, OrderId, OrderPrimaryAction, + OrderStatus, OrdersFilter, OrdersScreenQueryState, PackDayOutputOrderState, + PackDayProductTotalRow, PackDayScreenQueryState, PickupLocationId, TradeAgreementStatus, + TradeFulfillmentStatus, TradeInventoryStatus, TradePaymentDisplayStatus, + TradeRevisionStatus, TradeWorkflowSource, }; use rusqlite::{Connection, params}; @@ -1511,7 +1533,11 @@ mod tests { assert_eq!(detail.payment, TradePaymentDisplayStatus::NotRecorded); assert_eq!( detail.primary_action, - Some(OrderPrimaryAction::PublishReadyForPickup) + Some(OrderPrimaryAction::PublishPreparing) + ); + assert_eq!( + detail.fulfillment_actions, + OrderFulfillmentAction::ALL.to_vec() ); } @@ -1671,9 +1697,17 @@ mod tests { Some(OrderPrimaryAction::PublishDelivered) ); assert_eq!( + list.rows[0].fulfillment_actions, + OrderFulfillmentAction::ALL.to_vec() + ); + assert_eq!( detail.primary_action, Some(OrderPrimaryAction::PublishDelivered) ); + assert_eq!( + detail.fulfillment_actions, + OrderFulfillmentAction::ALL.to_vec() + ); } #[test] @@ -1731,7 +1765,9 @@ mod tests { Some(TradeFulfillmentStatus::Delivered) ); assert_eq!(list.rows[0].primary_action, None); + assert_eq!(list.rows[0].fulfillment_actions, Vec::new()); assert_eq!(detail.primary_action, None); + assert_eq!(detail.fulfillment_actions, Vec::new()); } #[test] diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -1686,18 +1686,87 @@ pub struct OrdersScreenQueryState { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] +pub enum OrderFulfillmentAction { + Preparing, + ReadyForPickup, + OutForDelivery, + Delivered, + SellerCancelled, +} + +impl OrderFulfillmentAction { + pub const ALL: &'static [Self] = &[ + Self::Preparing, + Self::ReadyForPickup, + Self::OutForDelivery, + Self::Delivered, + Self::SellerCancelled, + ]; + + pub const fn storage_key(self) -> &'static str { + match self { + Self::Preparing => "publish_preparing", + Self::ReadyForPickup => "publish_ready_for_pickup", + Self::OutForDelivery => "publish_out_for_delivery", + Self::Delivered => "publish_delivered", + Self::SellerCancelled => "publish_seller_cancelled", + } + } + + pub const fn fulfillment_status(self) -> RadrootsActiveTradeFulfillmentState { + match self { + Self::Preparing => RadrootsActiveTradeFulfillmentState::Preparing, + Self::ReadyForPickup => RadrootsActiveTradeFulfillmentState::ReadyForPickup, + Self::OutForDelivery => RadrootsActiveTradeFulfillmentState::OutForDelivery, + Self::Delivered => RadrootsActiveTradeFulfillmentState::Delivered, + Self::SellerCancelled => RadrootsActiveTradeFulfillmentState::SellerCancelled, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum OrderPrimaryAction { Review, + PublishPreparing, PublishReadyForPickup, + PublishOutForDelivery, PublishDelivered, + PublishSellerCancelled, } impl OrderPrimaryAction { pub const fn storage_key(self) -> &'static str { match self { Self::Review => "review", + Self::PublishPreparing => "publish_preparing", Self::PublishReadyForPickup => "publish_ready_for_pickup", + Self::PublishOutForDelivery => "publish_out_for_delivery", Self::PublishDelivered => "publish_delivered", + Self::PublishSellerCancelled => "publish_seller_cancelled", + } + } + + pub const fn fulfillment_action(self) -> Option<OrderFulfillmentAction> { + match self { + Self::Review => None, + Self::PublishPreparing => Some(OrderFulfillmentAction::Preparing), + Self::PublishReadyForPickup => Some(OrderFulfillmentAction::ReadyForPickup), + Self::PublishOutForDelivery => Some(OrderFulfillmentAction::OutForDelivery), + Self::PublishDelivered => Some(OrderFulfillmentAction::Delivered), + Self::PublishSellerCancelled => Some(OrderFulfillmentAction::SellerCancelled), + } + } +} + +impl From<OrderFulfillmentAction> for OrderPrimaryAction { + fn from(action: OrderFulfillmentAction) -> Self { + match action { + OrderFulfillmentAction::Preparing => Self::PublishPreparing, + OrderFulfillmentAction::ReadyForPickup => Self::PublishReadyForPickup, + OrderFulfillmentAction::OutForDelivery => Self::PublishOutForDelivery, + OrderFulfillmentAction::Delivered => Self::PublishDelivered, + OrderFulfillmentAction::SellerCancelled => Self::PublishSellerCancelled, } } } @@ -1728,6 +1797,7 @@ pub struct OrdersListRow { pub status: OrderStatus, pub workflow: TradeWorkflowProjection, pub primary_action: Option<OrderPrimaryAction>, + pub fulfillment_actions: Vec<OrderFulfillmentAction>, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -1765,6 +1835,7 @@ pub struct OrderDetailProjection { pub payment: TradePaymentDisplayStatus, pub workflow: TradeWorkflowProjection, pub primary_action: Option<OrderPrimaryAction>, + pub fulfillment_actions: Vec<OrderFulfillmentAction>, pub recoveries: Vec<OrderRecoveryProjection>, } @@ -2237,12 +2308,12 @@ mod tests { FarmSetupProjection, FarmSetupReadiness, FarmSetupSection, FarmTimingConflict, FarmTimingConflictKind, FarmerActivationProjection, FarmerSection, FulfillmentWindowId, IdentityBlockedReason, IdentityReadiness, LoggedOutStartupPhase, - LoggedOutStartupProjection, OrderDetailItemRow, OrderDetailProjection, OrderId, - OrderListRow, OrderPrimaryAction, OrderRecoveryProjection, OrderStatus, OrdersFilter, - OrdersListProjection, OrdersListRow, OrdersListSummary, OrdersScreenQueryState, - PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, - PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, - PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, + LoggedOutStartupProjection, OrderDetailItemRow, OrderDetailProjection, + OrderFulfillmentAction, OrderId, OrderListRow, OrderPrimaryAction, OrderRecoveryProjection, + OrderStatus, OrdersFilter, OrdersListProjection, OrdersListRow, OrdersListSummary, + OrdersScreenQueryState, PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, + PackDayBatchPrintStatus, PackDayExportArtifact, PackDayExportArtifactKind, + PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow, PackDayPackListRow, PackDayPrintFailureKind, @@ -2866,10 +2937,38 @@ mod tests { assert_eq!(OrderPrimaryAction::Review.storage_key(), "review"); assert_eq!( + OrderFulfillmentAction::Preparing.storage_key(), + "publish_preparing" + ); + assert_eq!( + OrderFulfillmentAction::ReadyForPickup.storage_key(), + "publish_ready_for_pickup" + ); + assert_eq!( + OrderFulfillmentAction::OutForDelivery.storage_key(), + "publish_out_for_delivery" + ); + assert_eq!( + OrderFulfillmentAction::Delivered.storage_key(), + "publish_delivered" + ); + assert_eq!( + OrderFulfillmentAction::SellerCancelled.storage_key(), + "publish_seller_cancelled" + ); + assert_eq!( + OrderFulfillmentAction::Preparing.fulfillment_status(), + RadrootsActiveTradeFulfillmentState::Preparing + ); + assert_eq!( OrderPrimaryAction::PublishReadyForPickup.storage_key(), "publish_ready_for_pickup" ); assert_eq!( + OrderPrimaryAction::PublishOutForDelivery.storage_key(), + "publish_out_for_delivery" + ); + assert_eq!( OrderPrimaryAction::PublishDelivered.storage_key(), "publish_delivered" ); @@ -3602,7 +3701,8 @@ mod tests { order_id, OrderStatus::Scheduled, ), - primary_action: Some(OrderPrimaryAction::PublishReadyForPickup), + primary_action: Some(OrderPrimaryAction::PublishPreparing), + fulfillment_actions: OrderFulfillmentAction::ALL.to_vec(), }], }; let order_detail = OrderDetailProjection { @@ -3628,7 +3728,8 @@ 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::PublishReadyForPickup), + primary_action: Some(OrderPrimaryAction::PublishPreparing), + fulfillment_actions: OrderFulfillmentAction::ALL.to_vec(), recoveries: Vec::new(), }; let pack_day = PackDayProjection { @@ -3658,7 +3759,11 @@ mod tests { assert!(!orders_list.is_empty()); assert_eq!( orders_list.rows[0].primary_action, - Some(OrderPrimaryAction::PublishReadyForPickup) + Some(OrderPrimaryAction::PublishPreparing) + ); + assert_eq!( + orders_list.rows[0].fulfillment_actions, + OrderFulfillmentAction::ALL.to_vec() ); assert_eq!( orders_list.rows[0].workflow.agreement, @@ -3833,6 +3938,7 @@ mod tests { OrderStatus::Completed, ), primary_action: None, + fulfillment_actions: Vec::new(), }; assert_eq!(today.orders_needing_action.len(), 1); diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -183,8 +183,12 @@ "orders.column.pickup": "Pickup", "orders.column.action": "Action", "orders.action.review": "Review", + "orders.action.preparing": "Preparing", "orders.action.ready_for_pickup": "Ready for pickup", + "orders.action.out_for_delivery": "Out for delivery", "orders.action.mark_delivered": "Mark delivered", + "orders.action.cancel_fulfillment": "Cancel fulfillment", + "orders.action.update_fulfillment": "Update", "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",