commit cdb4b18e136271bd37b058aed20a75281658e8c0
parent 5a8586cb55addfbac9c9acc24bc92062d979abd2
Author: triesap <tyson@radroots.org>
Date: Wed, 3 Jun 2026 00:14:09 -0700
app: show localized trade workflow badges
- add workflow projections to buyer and seller order rows and details
- derive badge axes from reducer-aligned order status and economics
- render localized agreement, fulfillment, inventory, payment, and source badges
- update localization keys, source guards, and projection fixtures
Diffstat:
9 files changed, 410 insertions(+), 114 deletions(-)
diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs
@@ -393,7 +393,6 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::PersonalOrdersDetailTitle",
"AppTextKey::PersonalOrdersDetailEmptyBody",
"AppTextKey::PersonalOrdersDetailFarmLabel",
- "AppTextKey::PersonalOrdersDetailStatusLabel",
"AppTextKey::PersonalOrdersDetailFulfillmentLabel",
"AppTextKey::PersonalOrdersDetailNoteLabel",
"AppTextKey::PersonalOrdersDetailItemsTitle",
@@ -403,11 +402,6 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::PersonalOrdersRepeatDemandNotePartialSingle",
"AppTextKey::PersonalOrdersRepeatDemandNotePartialMultiple",
"AppTextKey::PersonalOrdersRepeatDemandNoteUnavailable",
- "AppTextKey::PersonalOrdersStatusPlaced",
- "AppTextKey::PersonalOrdersStatusScheduled",
- "AppTextKey::PersonalOrdersStatusReady",
- "AppTextKey::PersonalOrdersStatusCompleted",
- "AppTextKey::PersonalOrdersStatusRefunded",
"AppTextKey::PersonalCartSurfaceBody",
"AppTextKey::PersonalOrderSummaryTitle",
"AppTextKey::PersonalFulfillmentTitle",
@@ -463,9 +457,14 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::OrdersDetailEmptyBody",
"AppTextKey::OrdersDetailItemsTitle",
"AppTextKey::OrdersDetailCustomerLabel",
- "AppTextKey::OrdersDetailStatusLabel",
"AppTextKey::OrdersDetailWindowLabel",
"AppTextKey::OrdersDetailPickupLabel",
+ "AppTextKey::TradeWorkflowAxisAgreement",
+ "AppTextKey::TradeWorkflowAxisRevision",
+ "AppTextKey::TradeWorkflowAxisFulfillment",
+ "AppTextKey::TradeWorkflowAxisInventory",
+ "AppTextKey::TradeWorkflowAxisPayment",
+ "AppTextKey::TradeWorkflowAxisSource",
"AppTextKey::OrdersRecoverySectionTitle",
"AppTextKey::OrdersRecoveryMissedPickupTitle",
"AppTextKey::OrdersRecoveryMissedPickupBody",
diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs
@@ -65,7 +65,9 @@ use radroots_app_view::{
RecoveryKind, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId,
ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency,
RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection,
- TodaySetupTaskKind, TradeEconomicsProjection, TradePaymentDisplayStatus,
+ TodaySetupTaskKind, TradeAgreementStatus, TradeEconomicsProjection, TradeFulfillmentStatus,
+ TradeInventoryStatus, TradePaymentDisplayStatus, TradeRevisionStatus, TradeWorkflowProjection,
+ TradeWorkflowSource,
};
use radroots_nostr::prelude::RadrootsNostrClient;
use std::{
@@ -4136,16 +4138,13 @@ impl HomeView {
app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
.child(app_heading_section(detail.order_number.clone()))
.child(home_body_text(detail.customer_display_name.clone()))
+ .child(trade_workflow_detail_badge_strip(&detail.workflow))
.child(label_value_list([
LabelValueRow::new(
app_shared_text(AppTextKey::OrdersDetailCustomerLabel),
detail.customer_display_name.clone(),
),
LabelValueRow::new(
- app_shared_text(AppTextKey::OrdersDetailStatusLabel),
- app_shared_text(orders_status_key(detail.status)),
- ),
- LabelValueRow::new(
app_shared_text(AppTextKey::OrdersDetailWindowLabel),
order_optional_text(detail.fulfillment_window_label.as_deref()),
),
@@ -4155,11 +4154,7 @@ impl HomeView {
),
LabelValueRow::new(
app_shared_text(AppTextKey::OrdersDetailTotalLabel),
- trade_economics_total_text(&detail.economics),
- ),
- LabelValueRow::new(
- app_shared_text(AppTextKey::OrdersDetailPaymentLabel),
- app_shared_text(trade_payment_display_status_key(detail.payment)),
+ trade_economics_total_text(&detail.workflow.economics),
),
]))
.child(app_form_section(
@@ -8597,6 +8592,141 @@ fn trade_economics_total_text(economics: &TradeEconomicsProjection) -> String {
.unwrap_or_else(|| app_shared_text(AppTextKey::ValueNone).to_string())
}
+fn trade_workflow_detail_badge_strip(workflow: &TradeWorkflowProjection) -> AnyElement {
+ let mut badges = vec![
+ trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisAgreement,
+ trade_agreement_status_key(workflow.agreement),
+ ),
+ trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisRevision,
+ trade_revision_status_key(workflow.revision),
+ ),
+ ];
+
+ if let Some(fulfillment) = workflow.fulfillment {
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisFulfillment,
+ trade_fulfillment_status_key(fulfillment),
+ ));
+ }
+
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisInventory,
+ trade_inventory_status_key(workflow.inventory),
+ ));
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisPayment,
+ trade_payment_display_status_key(workflow.payment),
+ ));
+ if workflow.provenance.primary_source != TradeWorkflowSource::Unknown {
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisSource,
+ trade_workflow_source_key(workflow.provenance.primary_source),
+ ));
+ }
+
+ app_cluster(APP_UI_THEME.foundation.spacing.small_px)
+ .w_full()
+ .children(badges)
+ .into_any_element()
+}
+
+fn trade_workflow_list_badge_strip(workflow: &TradeWorkflowProjection) -> AnyElement {
+ let mut badges = vec![trade_workflow_value_badge(trade_agreement_status_key(
+ workflow.agreement,
+ ))];
+
+ if workflow.revision != TradeRevisionStatus::None {
+ badges.push(trade_workflow_value_badge(trade_revision_status_key(
+ workflow.revision,
+ )));
+ }
+
+ if let Some(fulfillment) = workflow.fulfillment {
+ badges.push(trade_workflow_value_badge(trade_fulfillment_status_key(
+ fulfillment,
+ )));
+ }
+
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisPayment,
+ trade_payment_display_status_key(workflow.payment),
+ ));
+
+ app_cluster(APP_UI_THEME.foundation.spacing.tight_px)
+ .w_full()
+ .children(badges)
+ .into_any_element()
+}
+
+fn trade_workflow_status_stack(workflow: &TradeWorkflowProjection) -> AnyElement {
+ app_stack_v(2.0)
+ .min_w_0()
+ .child(trade_workflow_value_badge(trade_agreement_status_key(
+ workflow.agreement,
+ )))
+ .when_some(workflow.fulfillment, |this, fulfillment| {
+ this.child(trade_workflow_value_badge(trade_fulfillment_status_key(
+ fulfillment,
+ )))
+ })
+ .into_any_element()
+}
+
+fn trade_workflow_labeled_key_badge(label_key: AppTextKey, value_key: AppTextKey) -> AnyElement {
+ settings_badge_text(format!("{}: {}", app_text(label_key), app_text(value_key)))
+ .into_any_element()
+}
+
+fn trade_workflow_value_badge(value_key: AppTextKey) -> AnyElement {
+ settings_badge_text(app_shared_text(value_key)).into_any_element()
+}
+
+fn trade_agreement_status_key(status: TradeAgreementStatus) -> AppTextKey {
+ match status {
+ TradeAgreementStatus::Ordered => AppTextKey::TradeWorkflowAgreementOrdered,
+ TradeAgreementStatus::Confirmed => AppTextKey::TradeWorkflowAgreementConfirmed,
+ TradeAgreementStatus::Declined => AppTextKey::TradeWorkflowAgreementDeclined,
+ TradeAgreementStatus::Cancelled => AppTextKey::TradeWorkflowAgreementCancelled,
+ TradeAgreementStatus::Completed => AppTextKey::TradeWorkflowAgreementCompleted,
+ TradeAgreementStatus::NeedsReview => AppTextKey::TradeWorkflowAgreementNeedsReview,
+ }
+}
+
+fn trade_revision_status_key(status: TradeRevisionStatus) -> AppTextKey {
+ match status {
+ TradeRevisionStatus::None => AppTextKey::TradeWorkflowRevisionNone,
+ TradeRevisionStatus::ChangeProposed => AppTextKey::TradeWorkflowRevisionChangeProposed,
+ TradeRevisionStatus::Updated => AppTextKey::TradeWorkflowRevisionUpdated,
+ TradeRevisionStatus::KeptAsPlaced => AppTextKey::TradeWorkflowRevisionKeptAsPlaced,
+ }
+}
+
+fn trade_fulfillment_status_key(status: TradeFulfillmentStatus) -> AppTextKey {
+ match status {
+ TradeFulfillmentStatus::Confirmed => AppTextKey::TradeWorkflowFulfillmentConfirmed,
+ TradeFulfillmentStatus::Preparing => AppTextKey::TradeWorkflowFulfillmentPreparing,
+ TradeFulfillmentStatus::ReadyForPickup => {
+ AppTextKey::TradeWorkflowFulfillmentReadyForPickup
+ }
+ TradeFulfillmentStatus::OutForDelivery => {
+ AppTextKey::TradeWorkflowFulfillmentOutForDelivery
+ }
+ TradeFulfillmentStatus::Delivered => AppTextKey::TradeWorkflowFulfillmentDelivered,
+ TradeFulfillmentStatus::Cancelled => AppTextKey::TradeWorkflowFulfillmentCancelled,
+ }
+}
+
+fn trade_inventory_status_key(status: TradeInventoryStatus) -> AppTextKey {
+ match status {
+ TradeInventoryStatus::Available => AppTextKey::TradeWorkflowInventoryAvailable,
+ TradeInventoryStatus::Reserved => AppTextKey::TradeWorkflowInventoryReserved,
+ TradeInventoryStatus::SoldOut => AppTextKey::TradeWorkflowInventorySoldOut,
+ TradeInventoryStatus::NeedsReview => AppTextKey::TradeWorkflowInventoryNeedsReview,
+ }
+}
+
fn trade_payment_display_status_key(status: TradePaymentDisplayStatus) -> AppTextKey {
match status {
TradePaymentDisplayStatus::NotRecorded => AppTextKey::TradeWorkflowPaymentNotRecorded,
@@ -8605,6 +8735,16 @@ fn trade_payment_display_status_key(status: TradePaymentDisplayStatus) -> AppTex
}
}
+fn trade_workflow_source_key(source: TradeWorkflowSource) -> AppTextKey {
+ match source {
+ TradeWorkflowSource::App => AppTextKey::TradeWorkflowProvenanceApp,
+ TradeWorkflowSource::Cli => AppTextKey::TradeWorkflowProvenanceCli,
+ TradeWorkflowSource::Relay => AppTextKey::TradeWorkflowProvenanceRelay,
+ TradeWorkflowSource::LocalEvents => AppTextKey::TradeWorkflowProvenanceLocalEvents,
+ TradeWorkflowSource::Unknown => AppTextKey::TradeWorkflowProvenanceUnknown,
+ }
+}
+
fn buyer_orders_list_card(
rows: &[BuyerOrdersListRow],
selected_order_id: Option<OrderId>,
@@ -8703,10 +8843,13 @@ fn buyer_orders_list_entry(
.utility_title_text_px))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(rgb(APP_UI_THEME.foundation.text.primary))
- .child(app_shared_text(buyer_orders_status_key(row.status))),
+ .child(app_shared_text(trade_agreement_status_key(
+ row.workflow.agreement,
+ ))),
),
),
)
+ .child(trade_workflow_list_badge_strip(&row.workflow))
.child(buyer_listing_chip(row.fulfillment_summary.clone())),
)
.into_any_element()
@@ -8726,26 +8869,19 @@ fn buyer_order_detail_card(
.w_full()
.child(app_heading_section(detail.order_number.clone()))
.child(settings_badge_text(detail.farm_display_name.clone()))
+ .child(trade_workflow_detail_badge_strip(&detail.workflow))
.child(label_value_list([
LabelValueRow::new(
app_shared_text(AppTextKey::PersonalOrdersDetailFarmLabel),
detail.farm_display_name.clone(),
),
LabelValueRow::new(
- app_shared_text(AppTextKey::PersonalOrdersDetailStatusLabel),
- app_shared_text(buyer_orders_status_key(detail.status)),
- ),
- LabelValueRow::new(
app_shared_text(AppTextKey::PersonalOrdersDetailFulfillmentLabel),
detail.fulfillment_summary.clone(),
),
LabelValueRow::new(
app_shared_text(AppTextKey::PersonalOrdersDetailTotalLabel),
- trade_economics_total_text(&detail.economics),
- ),
- LabelValueRow::new(
- app_shared_text(AppTextKey::PersonalOrdersDetailPaymentLabel),
- app_shared_text(trade_payment_display_status_key(detail.payment)),
+ trade_economics_total_text(&detail.workflow.economics),
),
LabelValueRow::new(
app_shared_text(AppTextKey::PersonalOrdersDetailNoteLabel),
@@ -8885,17 +9021,6 @@ fn buyer_order_detail_empty_card() -> impl IntoElement {
)
}
-fn buyer_orders_status_key(status: BuyerOrderStatus) -> AppTextKey {
- match status {
- BuyerOrderStatus::Placed => AppTextKey::PersonalOrdersStatusPlaced,
- BuyerOrderStatus::Scheduled => AppTextKey::PersonalOrdersStatusScheduled,
- BuyerOrderStatus::Ready => AppTextKey::PersonalOrdersStatusReady,
- BuyerOrderStatus::Completed => AppTextKey::PersonalOrdersStatusCompleted,
- BuyerOrderStatus::Declined => AppTextKey::PersonalOrdersStatusDeclined,
- BuyerOrderStatus::Refunded => AppTextKey::PersonalOrdersStatusRefunded,
- }
-}
-
fn buyer_orders_status_color(status: BuyerOrderStatus) -> u32 {
match status {
BuyerOrderStatus::Placed => APP_UI_THEME.components.app_status_indicator.attention,
@@ -10055,7 +10180,7 @@ fn orders_table_header() -> impl IntoElement {
))
.child(products_table_header_column(
AppTextKey::OrdersColumnStatus,
- Some(128.0),
+ Some(160.0),
false,
))
.child(products_table_header_column(
@@ -10088,17 +10213,12 @@ fn orders_table_row(
.child(order)
.child(
div()
- .w(px(128.0))
+ .w(px(160.0))
.flex()
- .items_center()
+ .items_start()
.gap(px(6.0))
.child(status_indicator(orders_status_color(row.status)))
- .child(
- div()
- .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
- .text_color(rgb(APP_UI_THEME.foundation.text.primary))
- .child(app_shared_text(orders_status_key(row.status))),
- ),
+ .child(trade_workflow_status_stack(&row.workflow)),
)
.child(
div()
@@ -10170,17 +10290,6 @@ fn orders_empty_state_card(filter: OrdersFilter) -> impl IntoElement {
home_empty_state_card(title_key, body_key)
}
-fn orders_status_key(status: OrderStatus) -> AppTextKey {
- match status {
- OrderStatus::NeedsAction => AppTextKey::OrdersStatusNeedsAction,
- OrderStatus::Scheduled => AppTextKey::OrdersStatusScheduled,
- OrderStatus::Packed => AppTextKey::OrdersStatusPacked,
- OrderStatus::Completed => AppTextKey::OrdersStatusCompleted,
- OrderStatus::Declined => AppTextKey::OrdersStatusDeclined,
- OrderStatus::Refunded => AppTextKey::OrdersStatusRefunded,
- }
-}
-
fn orders_status_color(status: OrderStatus) -> u32 {
match status {
OrderStatus::NeedsAction => APP_UI_THEME.components.app_status_indicator.attention,
@@ -13091,8 +13200,8 @@ mod tests {
about_conflict_detail_rows, about_conflict_review_body_key, about_manual_refresh_enabled,
about_runtime_rows, about_status_rows, app_text,
buyer_order_coordination_notice_forces_redraw, buyer_orders_retry_action_visible,
- buyer_orders_status_key, farm_setup_onboarding_card_spec, farmer_home_farm_state,
- farmer_pack_day_available, home_auto_focus_target, home_content_scroll_id, home_saved_farm,
+ farm_setup_onboarding_card_spec, farmer_home_farm_state, farmer_pack_day_available,
+ home_auto_focus_target, home_content_scroll_id, home_saved_farm,
home_sidebar_navigation_sections, home_stage, home_window_launch_size_px,
home_window_minimum_size_px, pack_day_batch_print_action_presentation,
pack_day_batch_print_status_presentation, pack_day_export_action_enabled,
@@ -13143,7 +13252,7 @@ mod tests {
ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, ReminderKind,
ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection,
ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
- TradeEconomicsProjection, TradePaymentDisplayStatus,
+ TradeEconomicsProjection, TradePaymentDisplayStatus, TradeWorkflowProjection,
};
use radroots_identity::RadrootsIdentity;
use std::{
@@ -13609,6 +13718,10 @@ mod tests {
farm_display_name: String::new(),
fulfillment_summary: String::new(),
status: BuyerOrderStatus::Placed,
+ workflow: TradeWorkflowProjection::from_buyer_order_status(
+ order_id,
+ BuyerOrderStatus::Placed,
+ ),
repeat_demand: None,
}];
buyer_orders.personal_projection.orders.detail = Some(BuyerOrderDetailProjection {
@@ -13621,6 +13734,10 @@ mod tests {
items: Vec::new(),
economics: TradeEconomicsProjection::default(),
payment: TradePaymentDisplayStatus::NotRecorded,
+ workflow: TradeWorkflowProjection::from_buyer_order_status(
+ order_id,
+ BuyerOrderStatus::Placed,
+ ),
order_note: None,
repeat_demand: Some(RepeatDemandHandoffProjection {
order_id,
@@ -13725,20 +13842,26 @@ mod tests {
ActiveSurface::Farmer,
ShellSection::Farmer(FarmerSection::Orders),
);
+ let farmer_order_id = OrderId::new();
+ let farmer_order_farm_id = FarmId::new();
orders.orders_projection.list.rows = vec![OrdersListRow {
- order_id: OrderId::new(),
- farm_id: FarmId::new(),
+ order_id: farmer_order_id,
+ farm_id: farmer_order_farm_id,
fulfillment_window_id: None,
order_number: String::new(),
customer_display_name: String::new(),
fulfillment_window_label: None,
pickup_location_label: None,
status: OrderStatus::Scheduled,
+ workflow: TradeWorkflowProjection::from_order_status(
+ farmer_order_id,
+ OrderStatus::Scheduled,
+ ),
primary_action: Some(OrderPrimaryAction::MarkPacked),
}];
orders.orders_projection.detail = Some(OrderDetailProjection {
- order_id: OrderId::new(),
- farm_id: FarmId::new(),
+ order_id: farmer_order_id,
+ farm_id: farmer_order_farm_id,
order_number: String::new(),
customer_display_name: String::new(),
status: OrderStatus::Scheduled,
@@ -13748,6 +13871,10 @@ mod tests {
items: Vec::new(),
economics: TradeEconomicsProjection::default(),
payment: TradePaymentDisplayStatus::NotRecorded,
+ workflow: TradeWorkflowProjection::from_order_status(
+ farmer_order_id,
+ OrderStatus::Scheduled,
+ ),
primary_action: Some(OrderPrimaryAction::MarkPacked),
recoveries: Vec::new(),
});
@@ -13794,18 +13921,6 @@ mod tests {
}
#[test]
- fn buyer_orders_status_keys_use_buyer_facing_copy() {
- assert_eq!(
- buyer_orders_status_key(BuyerOrderStatus::Placed),
- AppTextKey::PersonalOrdersStatusPlaced
- );
- assert_eq!(
- buyer_orders_status_key(BuyerOrderStatus::Ready),
- AppTextKey::PersonalOrdersStatusReady
- );
- }
-
- #[test]
fn farmer_home_farm_state_distinguishes_no_farm_incomplete_and_configured() {
let farm_id = FarmId::new();
let incomplete_farm = FarmSummary {
diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs
@@ -227,6 +227,12 @@ define_app_text_keys! {
OrdersRecoveryStateOpen => "orders.recovery.state.open",
OrdersRecoveryStateInReview => "orders.recovery.state.in_review",
OrdersRecoveryStateResolved => "orders.recovery.state.resolved",
+ TradeWorkflowAxisAgreement => "trade.workflow.axis.agreement",
+ TradeWorkflowAxisRevision => "trade.workflow.axis.revision",
+ TradeWorkflowAxisFulfillment => "trade.workflow.axis.fulfillment",
+ TradeWorkflowAxisInventory => "trade.workflow.axis.inventory",
+ TradeWorkflowAxisPayment => "trade.workflow.axis.payment",
+ TradeWorkflowAxisSource => "trade.workflow.axis.source",
TradeWorkflowAgreementOrdered => "trade.workflow.agreement.ordered",
TradeWorkflowAgreementConfirmed => "trade.workflow.agreement.confirmed",
TradeWorkflowAgreementDeclined => "trade.workflow.agreement.declined",
diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs
@@ -380,6 +380,16 @@ mod tests {
#[test]
fn english_trade_workflow_copy_matches_the_projection_contract() {
assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAxisAgreement),
+ "Agreement"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAxisFulfillment),
+ "Fulfillment"
+ );
+ assert_eq!(app_text(AppTextKey::TradeWorkflowAxisPayment), "Payment");
+ assert_eq!(app_text(AppTextKey::TradeWorkflowAxisSource), "Source");
+ assert_eq!(
app_text(AppTextKey::TradeWorkflowAgreementOrdered),
"Ordered"
);
diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs
@@ -2275,6 +2275,7 @@ mod tests {
ReminderLogEntryProjection, ReminderLogProjection, SelectedAccountProjection,
SelectedSurfaceProjection, SettingsSection, ShellSection, TodayAgendaProjection,
TodaySetupTask, TodaySetupTaskKind, TradeEconomicsProjection, TradePaymentDisplayStatus,
+ TradeWorkflowProjection,
};
struct FailingRepository;
@@ -2500,6 +2501,13 @@ mod tests {
let farm_id = FarmId::new();
let fulfillment_window_id = FulfillmentWindowId::new();
let order_id = OrderId::new();
+ let order_economics = TradeEconomicsProjection {
+ subtotal_minor_units: Some(1300),
+ total_minor_units: Some(1300),
+ currency_code: Some("USD".to_owned()),
+ ..TradeEconomicsProjection::default()
+ };
+ let order_payment = TradePaymentDisplayStatus::NotRecorded;
let orders_list = OrdersListProjection {
summary: OrdersListSummary {
total_orders: 2,
@@ -2516,6 +2524,10 @@ mod tests {
fulfillment_window_label: Some("Friday pickup".to_owned()),
pickup_location_label: Some("North barn".to_owned()),
status: OrderStatus::NeedsAction,
+ workflow: TradeWorkflowProjection::from_order_status(
+ order_id,
+ OrderStatus::NeedsAction,
+ ),
primary_action: Some(OrderPrimaryAction::Review),
}],
};
@@ -2538,13 +2550,13 @@ mod tests {
}),
line_total_minor_units: Some(1300),
}],
- economics: TradeEconomicsProjection {
- subtotal_minor_units: Some(1300),
- total_minor_units: Some(1300),
- currency_code: Some("USD".to_owned()),
- ..TradeEconomicsProjection::default()
- },
- payment: TradePaymentDisplayStatus::NotRecorded,
+ economics: order_economics.clone(),
+ payment: order_payment,
+ workflow: TradeWorkflowProjection::from_order_status(
+ order_id,
+ OrderStatus::NeedsAction,
+ )
+ .with_economics_and_payment(order_economics, order_payment),
primary_action: Some(OrderPrimaryAction::Review),
recoveries: Vec::new(),
};
diff --git a/crates/store/src/repo/buyer.rs b/crates/store/src/repo/buyer.rs
@@ -9,6 +9,7 @@ use radroots_app_view::{
OrderId, OrderStatus, ProductAvailabilityState, ProductAvailabilitySummary, ProductId,
ProductPricePresentation, ProductStatus, ProductStockState, ProductStockSummary,
RepeatDemandEligibility, RepeatDemandHandoffProjection, TradePaymentDisplayStatus,
+ TradeWorkflowProjection,
};
use rusqlite::{Connection, OptionalExtension, params};
use serde_json::Value;
@@ -786,14 +787,17 @@ impl<'a> AppBuyerRepository<'a> {
operation: "read buyer orders list",
source,
})?;
+ let order_id = parse_typed_id("orders.id", order_id)?;
+ let farm_id = parse_typed_id("orders.farm_id", farm_id)?;
+ let buyer_status = BuyerOrderStatus::from(parse_order_status("orders.status", status)?);
orders.push(BuyerOrdersListRow {
- order_id: parse_typed_id("orders.id", order_id.clone())?,
- farm_id: parse_typed_id("orders.farm_id", farm_id.clone())?,
+ order_id,
+ farm_id,
order_number,
repeat_demand: self.build_repeat_demand_handoff(
- parse_typed_id("orders.id", order_id)?,
- parse_typed_id("orders.farm_id", farm_id)?,
+ order_id,
+ farm_id,
farm_display_name.as_str(),
&visible_listings,
)?,
@@ -803,7 +807,8 @@ impl<'a> AppBuyerRepository<'a> {
fulfillment_starts_at,
fulfillment_ends_at,
),
- status: BuyerOrderStatus::from(parse_order_status("orders.status", status)?),
+ status: buyer_status,
+ workflow: TradeWorkflowProjection::from_buyer_order_status(order_id, buyer_status),
});
}
@@ -872,8 +877,14 @@ impl<'a> AppBuyerRepository<'a> {
)| {
let order_id: OrderId = parse_typed_id("orders.id", order_id)?;
let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?;
+ let status =
+ BuyerOrderStatus::from(parse_order_status("orders.status", status)?);
let items = self.load_order_detail_items(order_id.to_string())?;
let economics = order_detail_economics(&items)?;
+ let payment = TradePaymentDisplayStatus::NotRecorded;
+ let workflow =
+ TradeWorkflowProjection::from_buyer_order_status(order_id, status)
+ .with_economics_and_payment(economics.clone(), payment);
Ok(BuyerOrderDetailProjection {
order_id,
farm_id,
@@ -884,13 +895,11 @@ impl<'a> AppBuyerRepository<'a> {
fulfillment_starts_at,
fulfillment_ends_at,
),
- status: BuyerOrderStatus::from(parse_order_status(
- "orders.status",
- status,
- )?),
+ status,
items,
economics,
- payment: TradePaymentDisplayStatus::NotRecorded,
+ payment,
+ workflow,
order_note: empty_string_to_none(order_note),
repeat_demand: self.build_repeat_demand_handoff(
order_id,
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, TradePaymentDisplayStatus,
+ PackDayScreenQueryState, ProductId, TradePaymentDisplayStatus, TradeWorkflowProjection,
};
use rusqlite::{Connection, OptionalExtension, params};
@@ -113,12 +113,17 @@ impl<'a> AppOrdersRepository<'a> {
fulfillment_window_label,
pickup_location_label,
)| {
+ let order_id: OrderId = parse_typed_id("orders.id", order_id)?;
+ let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?;
let status = parse_order_status("orders.status", status)?;
- let items = self.load_order_detail_items(order_id.clone())?;
+ let items = self.load_order_detail_items(order_id.to_string())?;
let economics = order_detail_economics(&items)?;
+ let payment = TradePaymentDisplayStatus::NotRecorded;
+ let workflow = TradeWorkflowProjection::from_order_status(order_id, status)
+ .with_economics_and_payment(economics.clone(), payment);
Ok(OrderDetailProjection {
- order_id: parse_typed_id("orders.id", order_id)?,
- farm_id: parse_typed_id("orders.farm_id", farm_id)?,
+ order_id,
+ farm_id,
order_number,
customer_display_name,
status,
@@ -130,7 +135,8 @@ impl<'a> AppOrdersRepository<'a> {
pickup_location_label: empty_string_to_none(pickup_location_label),
items,
economics,
- payment: TradePaymentDisplayStatus::NotRecorded,
+ payment,
+ workflow,
primary_action: primary_action_for_status(status),
recoveries: Vec::new(),
})
@@ -1126,6 +1132,8 @@ impl OrderRecord {
}
fn into_list_row(self) -> OrdersListRow {
+ let workflow = TradeWorkflowProjection::from_order_status(self.order_id, self.status);
+
OrdersListRow {
order_id: self.order_id,
farm_id: self.farm_id,
@@ -1135,6 +1143,7 @@ impl OrderRecord {
fulfillment_window_label: self.fulfillment_window_label,
pickup_location_label: self.pickup_location_label,
status: self.status,
+ workflow,
primary_action: primary_action_for_status(self.status),
}
}
diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs
@@ -1448,6 +1448,96 @@ impl TradeWorkflowProjection {
TradeAgreementStatus::from_reducer_status(agreement),
)
}
+
+ pub fn from_order_status(order_id: OrderId, status: OrderStatus) -> Self {
+ let mut projection = match status {
+ OrderStatus::NeedsAction => Self::new(order_id, TradeAgreementStatus::Ordered),
+ OrderStatus::Scheduled => Self::new(order_id, TradeAgreementStatus::Confirmed),
+ OrderStatus::Packed => Self::new(order_id, TradeAgreementStatus::Confirmed),
+ OrderStatus::Completed => Self::new(order_id, TradeAgreementStatus::Completed),
+ OrderStatus::Declined => Self::new(order_id, TradeAgreementStatus::Declined),
+ OrderStatus::Refunded => Self::new(order_id, TradeAgreementStatus::NeedsReview),
+ };
+
+ match status {
+ OrderStatus::NeedsAction => {}
+ OrderStatus::Scheduled => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::Confirmed);
+ projection.inventory = TradeInventoryStatus::Reserved;
+ }
+ OrderStatus::Packed => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::ReadyForPickup);
+ projection.inventory = TradeInventoryStatus::Reserved;
+ }
+ OrderStatus::Completed => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::Delivered);
+ projection.inventory = TradeInventoryStatus::Reserved;
+ }
+ OrderStatus::Declined => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::Cancelled);
+ projection.inventory = TradeInventoryStatus::Available;
+ }
+ OrderStatus::Refunded => {
+ projection.payment = TradePaymentDisplayStatus::NeedsReview;
+ }
+ }
+
+ projection
+ }
+
+ pub fn from_buyer_order_status(order_id: OrderId, status: BuyerOrderStatus) -> Self {
+ let mut projection = match status {
+ BuyerOrderStatus::Placed => Self::new(order_id, TradeAgreementStatus::Ordered),
+ BuyerOrderStatus::Scheduled => Self::new(order_id, TradeAgreementStatus::Confirmed),
+ BuyerOrderStatus::Ready => Self::new(order_id, TradeAgreementStatus::Confirmed),
+ BuyerOrderStatus::Completed => Self::new(order_id, TradeAgreementStatus::Completed),
+ BuyerOrderStatus::Declined => Self::new(order_id, TradeAgreementStatus::Declined),
+ BuyerOrderStatus::Refunded => Self::new(order_id, TradeAgreementStatus::NeedsReview),
+ };
+
+ match status {
+ BuyerOrderStatus::Placed => {}
+ BuyerOrderStatus::Scheduled => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::Confirmed);
+ projection.inventory = TradeInventoryStatus::Reserved;
+ }
+ BuyerOrderStatus::Ready => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::ReadyForPickup);
+ projection.inventory = TradeInventoryStatus::Reserved;
+ }
+ BuyerOrderStatus::Completed => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::Delivered);
+ projection.inventory = TradeInventoryStatus::Reserved;
+ }
+ BuyerOrderStatus::Declined => {
+ projection.fulfillment = Some(TradeFulfillmentStatus::Cancelled);
+ projection.inventory = TradeInventoryStatus::Available;
+ }
+ BuyerOrderStatus::Refunded => {
+ projection.payment = TradePaymentDisplayStatus::NeedsReview;
+ }
+ }
+
+ projection
+ }
+
+ pub fn with_economics(mut self, economics: TradeEconomicsProjection) -> Self {
+ self.economics = economics;
+ self
+ }
+
+ pub fn with_payment(mut self, payment: TradePaymentDisplayStatus) -> Self {
+ self.payment = payment;
+ self
+ }
+
+ pub fn with_economics_and_payment(
+ self,
+ economics: TradeEconomicsProjection,
+ payment: TradePaymentDisplayStatus,
+ ) -> Self {
+ self.with_economics(economics).with_payment(payment)
+ }
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
@@ -1523,6 +1613,7 @@ pub struct OrdersListRow {
pub fulfillment_window_label: Option<String>,
pub pickup_location_label: Option<String>,
pub status: OrderStatus,
+ pub workflow: TradeWorkflowProjection,
pub primary_action: Option<OrderPrimaryAction>,
}
@@ -1559,6 +1650,7 @@ pub struct OrderDetailProjection {
pub items: Vec<OrderDetailItemRow>,
pub economics: TradeEconomicsProjection,
pub payment: TradePaymentDisplayStatus,
+ pub workflow: TradeWorkflowProjection,
pub primary_action: Option<OrderPrimaryAction>,
pub recoveries: Vec<OrderRecoveryProjection>,
}
@@ -1571,6 +1663,7 @@ pub struct BuyerOrdersListRow {
pub farm_display_name: String,
pub fulfillment_summary: String,
pub status: BuyerOrderStatus,
+ pub workflow: TradeWorkflowProjection,
pub repeat_demand: Option<RepeatDemandHandoffProjection>,
}
@@ -1596,6 +1689,7 @@ pub struct BuyerOrderDetailProjection {
pub items: Vec<OrderDetailItemRow>,
pub economics: TradeEconomicsProjection,
pub payment: TradePaymentDisplayStatus,
+ pub workflow: TradeWorkflowProjection,
pub order_note: Option<String>,
pub repeat_demand: Option<RepeatDemandHandoffProjection>,
}
@@ -3101,6 +3195,13 @@ mod tests {
let fulfillment_window_id = super::FulfillmentWindowId::new();
let farm_id = FarmId::new();
let order_id = super::OrderId::new();
+ let order_economics = TradeEconomicsProjection {
+ subtotal_minor_units: Some(1300),
+ total_minor_units: Some(1300),
+ currency_code: Some("USD".to_owned()),
+ ..TradeEconomicsProjection::default()
+ };
+ let order_payment = TradePaymentDisplayStatus::NotRecorded;
let orders_list = OrdersListProjection {
summary: OrdersListSummary {
total_orders: 3,
@@ -3117,6 +3218,10 @@ mod tests {
fulfillment_window_label: Some("Wednesday pickup".to_owned()),
pickup_location_label: Some("North barn".to_owned()),
status: OrderStatus::Scheduled,
+ workflow: TradeWorkflowProjection::from_order_status(
+ order_id,
+ OrderStatus::Scheduled,
+ ),
primary_action: Some(OrderPrimaryAction::MarkPacked),
}],
};
@@ -3139,13 +3244,10 @@ mod tests {
}),
line_total_minor_units: Some(1300),
}],
- economics: TradeEconomicsProjection {
- subtotal_minor_units: Some(1300),
- total_minor_units: Some(1300),
- currency_code: Some("USD".to_owned()),
- ..TradeEconomicsProjection::default()
- },
- payment: TradePaymentDisplayStatus::NotRecorded,
+ economics: order_economics.clone(),
+ 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),
recoveries: Vec::new(),
};
@@ -3178,7 +3280,15 @@ mod tests {
orders_list.rows[0].primary_action,
Some(OrderPrimaryAction::MarkPacked)
);
+ assert_eq!(
+ orders_list.rows[0].workflow.agreement,
+ TradeAgreementStatus::Confirmed
+ );
assert_eq!(order_detail.items[0].quantity_display, "2 bags");
+ assert_eq!(
+ order_detail.workflow.fulfillment,
+ Some(TradeFulfillmentStatus::Confirmed)
+ );
assert!(!pack_day.is_empty());
assert_eq!(pack_day.pickup_roster[0].order_number, "R-1001");
}
@@ -3188,6 +3298,13 @@ mod tests {
let farm_id = FarmId::new();
let product_id = super::ProductId::new();
let order_id = super::OrderId::new();
+ let buyer_order_economics = TradeEconomicsProjection {
+ subtotal_minor_units: Some(1300),
+ total_minor_units: Some(1300),
+ currency_code: Some("USD".to_owned()),
+ ..TradeEconomicsProjection::default()
+ };
+ let buyer_order_payment = TradePaymentDisplayStatus::NotRecorded;
let listing = BuyerListingRow {
product_id,
farm_id,
@@ -3261,6 +3378,10 @@ mod tests {
farm_display_name: "Cedar Grove Farm".to_owned(),
fulfillment_summary: "Thursday pickup".to_owned(),
status: BuyerOrderStatus::Scheduled,
+ workflow: TradeWorkflowProjection::from_buyer_order_status(
+ order_id,
+ BuyerOrderStatus::Scheduled,
+ ),
repeat_demand: None,
}],
};
@@ -3281,13 +3402,13 @@ mod tests {
}),
line_total_minor_units: Some(1300),
}],
- economics: TradeEconomicsProjection {
- subtotal_minor_units: Some(1300),
- total_minor_units: Some(1300),
- currency_code: Some("USD".to_owned()),
- ..TradeEconomicsProjection::default()
- },
- payment: TradePaymentDisplayStatus::NotRecorded,
+ economics: buyer_order_economics.clone(),
+ payment: buyer_order_payment,
+ workflow: TradeWorkflowProjection::from_buyer_order_status(
+ order_id,
+ BuyerOrderStatus::Scheduled,
+ )
+ .with_economics_and_payment(buyer_order_economics, buyer_order_payment),
order_note: Some("Leave by the cooler".to_owned()),
repeat_demand: None,
};
@@ -3298,6 +3419,10 @@ mod tests {
assert!(!orders.is_empty());
assert_eq!(listing.fulfillment_methods.len(), 1);
assert_eq!(order_detail.status, BuyerOrderStatus::Scheduled);
+ assert_eq!(
+ order_detail.workflow.agreement,
+ TradeAgreementStatus::Confirmed
+ );
}
#[test]
@@ -3313,8 +3438,9 @@ mod tests {
}],
..TodayAgendaProjection::default()
};
+ let orders_row_id = super::OrderId::new();
let orders_row = OrdersListRow {
- order_id: super::OrderId::new(),
+ order_id: orders_row_id,
farm_id: FarmId::new(),
fulfillment_window_id: None,
order_number: "R-2002".to_owned(),
@@ -3322,6 +3448,10 @@ mod tests {
fulfillment_window_label: None,
pickup_location_label: None,
status: OrderStatus::Completed,
+ workflow: TradeWorkflowProjection::from_order_status(
+ orders_row_id,
+ OrderStatus::Completed,
+ ),
primary_action: None,
};
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -207,6 +207,12 @@
"orders.recovery.state.open": "Open",
"orders.recovery.state.in_review": "In review",
"orders.recovery.state.resolved": "Resolved",
+ "trade.workflow.axis.agreement": "Agreement",
+ "trade.workflow.axis.revision": "Change",
+ "trade.workflow.axis.fulfillment": "Fulfillment",
+ "trade.workflow.axis.inventory": "Stock",
+ "trade.workflow.axis.payment": "Payment",
+ "trade.workflow.axis.source": "Source",
"trade.workflow.agreement.ordered": "Ordered",
"trade.workflow.agreement.confirmed": "Confirmed",
"trade.workflow.agreement.declined": "Declined",