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