commit 62ce4184e6152feb66845441f911e3388225edf9
parent fbb1c2bc91c38023ba3a7c99ba7886a3afcae477
Author: triesap <tyson@radroots.org>
Date: Tue, 2 Jun 2026 22:50:18 -0700
view: define trade workflow projection contract
- add reducer-derived trade workflow projection axes
- localize agreement, revision, fulfillment, inventory, payment, and provenance labels
- replace visible checkout and refund handling copy with order status language
- cover projection mappings and catalog labels with focused tests
Diffstat:
4 files changed, 635 insertions(+), 10 deletions(-)
diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs
@@ -223,6 +223,34 @@ define_app_text_keys! {
OrdersRecoveryStateOpen => "orders.recovery.state.open",
OrdersRecoveryStateInReview => "orders.recovery.state.in_review",
OrdersRecoveryStateResolved => "orders.recovery.state.resolved",
+ TradeWorkflowAgreementOrdered => "trade.workflow.agreement.ordered",
+ TradeWorkflowAgreementConfirmed => "trade.workflow.agreement.confirmed",
+ TradeWorkflowAgreementDeclined => "trade.workflow.agreement.declined",
+ TradeWorkflowAgreementCancelled => "trade.workflow.agreement.cancelled",
+ TradeWorkflowAgreementCompleted => "trade.workflow.agreement.completed",
+ TradeWorkflowAgreementNeedsReview => "trade.workflow.agreement.needs_review",
+ TradeWorkflowRevisionNone => "trade.workflow.revision.none",
+ TradeWorkflowRevisionChangeProposed => "trade.workflow.revision.change_proposed",
+ TradeWorkflowRevisionUpdated => "trade.workflow.revision.updated",
+ TradeWorkflowRevisionKeptAsPlaced => "trade.workflow.revision.kept_as_placed",
+ TradeWorkflowFulfillmentConfirmed => "trade.workflow.fulfillment.confirmed",
+ TradeWorkflowFulfillmentPreparing => "trade.workflow.fulfillment.preparing",
+ TradeWorkflowFulfillmentReadyForPickup => "trade.workflow.fulfillment.ready_for_pickup",
+ TradeWorkflowFulfillmentOutForDelivery => "trade.workflow.fulfillment.out_for_delivery",
+ TradeWorkflowFulfillmentDelivered => "trade.workflow.fulfillment.delivered",
+ TradeWorkflowFulfillmentCancelled => "trade.workflow.fulfillment.cancelled",
+ TradeWorkflowInventoryAvailable => "trade.workflow.inventory.available",
+ TradeWorkflowInventoryReserved => "trade.workflow.inventory.reserved",
+ TradeWorkflowInventorySoldOut => "trade.workflow.inventory.sold_out",
+ TradeWorkflowInventoryNeedsReview => "trade.workflow.inventory.needs_review",
+ TradeWorkflowPaymentNotRecorded => "trade.workflow.payment.not_recorded",
+ TradeWorkflowPaymentRecorded => "trade.workflow.payment.recorded",
+ TradeWorkflowPaymentNeedsReview => "trade.workflow.payment.needs_review",
+ TradeWorkflowProvenanceApp => "trade.workflow.provenance.app",
+ TradeWorkflowProvenanceCli => "trade.workflow.provenance.cli",
+ TradeWorkflowProvenanceRelay => "trade.workflow.provenance.relay",
+ TradeWorkflowProvenanceLocalEvents => "trade.workflow.provenance.local_events",
+ TradeWorkflowProvenanceUnknown => "trade.workflow.provenance.unknown",
OrdersRemindersTitle => "orders.reminders.title",
OrdersReminderLogTitle => "orders.reminder_log.title",
OrdersReminderLogEmptyBody => "orders.reminder_log.empty.body",
diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs
@@ -311,7 +311,11 @@ mod tests {
);
assert_eq!(
app_text(AppTextKey::OrdersRecoveryRefundFollowUpTitle),
- "Refund follow-up"
+ "Payment status"
+ );
+ assert_eq!(
+ app_text(AppTextKey::OrdersRecoveryRefundFollowUpBody),
+ "Track the recorded payment state for this order."
);
assert_eq!(
app_text(AppTextKey::OrdersRecoveryActionResolve),
@@ -352,16 +356,16 @@ mod tests {
fn english_marketplace_checkout_copy_matches_the_local_order_contract() {
assert_eq!(
app_text(AppTextKey::PersonalCartContinueCheckoutAction),
- "Continue to checkout"
+ "Review order"
);
- assert_eq!(app_text(AppTextKey::PersonalCheckoutTitle), "Checkout");
+ assert_eq!(app_text(AppTextKey::PersonalCheckoutTitle), "Order review");
assert_eq!(
app_text(AppTextKey::PersonalCheckoutPlaceOrderAction),
"Place order"
);
assert_eq!(
app_text(AppTextKey::PersonalCheckoutLocalOnlyBody),
- "This places a local order on this device. It does not charge a card."
+ "Review the details before placing the order."
);
assert_eq!(
app_text(AppTextKey::PersonalOrderPlaceFailedNotice),
@@ -374,6 +378,51 @@ mod tests {
}
#[test]
+ fn english_trade_workflow_copy_matches_the_projection_contract() {
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAgreementOrdered),
+ "Ordered"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAgreementConfirmed),
+ "Confirmed"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAgreementNeedsReview),
+ "Needs review"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowRevisionChangeProposed),
+ "Change proposed"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowRevisionKeptAsPlaced),
+ "Kept as placed"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowFulfillmentReadyForPickup),
+ "Ready for pickup"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowInventoryReserved),
+ "Reserved"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowPaymentNotRecorded),
+ "Not recorded"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowPaymentRecorded),
+ "Recorded"
+ );
+ assert_eq!(app_text(AppTextKey::TradeWorkflowProvenanceCli), "CLI");
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowProvenanceLocalEvents),
+ "Local events"
+ );
+ }
+
+ #[test]
fn english_marketplace_orders_copy_matches_the_buyer_history_contract() {
assert_eq!(
app_text(AppTextKey::PersonalOrdersSurfaceBody),
diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs
@@ -1058,6 +1058,398 @@ pub struct BuyerCheckoutProjection {
pub place_order_disabled_reason: Option<BuyerCheckoutDisabledReason>,
}
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeReducerAgreementStatus {
+ Requested,
+ Accepted,
+ Declined,
+ Cancelled,
+ Completed,
+ Disputed,
+ Invalid,
+}
+
+impl TradeReducerAgreementStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::Requested => "requested",
+ Self::Accepted => "accepted",
+ Self::Declined => "declined",
+ Self::Cancelled => "cancelled",
+ Self::Completed => "completed",
+ Self::Disputed => "disputed",
+ Self::Invalid => "invalid",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeAgreementStatus {
+ #[default]
+ Ordered,
+ Confirmed,
+ Declined,
+ Cancelled,
+ Completed,
+ NeedsReview,
+}
+
+impl TradeAgreementStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::Ordered => "ordered",
+ Self::Confirmed => "confirmed",
+ Self::Declined => "declined",
+ Self::Cancelled => "cancelled",
+ Self::Completed => "completed",
+ Self::NeedsReview => "needs_review",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::Ordered => "messages.trade.workflow.agreement.ordered",
+ Self::Confirmed => "messages.trade.workflow.agreement.confirmed",
+ Self::Declined => "messages.trade.workflow.agreement.declined",
+ Self::Cancelled => "messages.trade.workflow.agreement.cancelled",
+ Self::Completed => "messages.trade.workflow.agreement.completed",
+ Self::NeedsReview => "messages.trade.workflow.agreement.needs_review",
+ }
+ }
+
+ pub const fn from_reducer_status(status: TradeReducerAgreementStatus) -> Self {
+ match status {
+ TradeReducerAgreementStatus::Requested => Self::Ordered,
+ TradeReducerAgreementStatus::Accepted => Self::Confirmed,
+ TradeReducerAgreementStatus::Declined => Self::Declined,
+ TradeReducerAgreementStatus::Cancelled => Self::Cancelled,
+ TradeReducerAgreementStatus::Completed => Self::Completed,
+ TradeReducerAgreementStatus::Disputed | TradeReducerAgreementStatus::Invalid => {
+ Self::NeedsReview
+ }
+ }
+ }
+}
+
+impl From<TradeReducerAgreementStatus> for TradeAgreementStatus {
+ fn from(status: TradeReducerAgreementStatus) -> Self {
+ Self::from_reducer_status(status)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeReducerRevisionStatus {
+ None,
+ Proposed,
+ Accepted,
+ Declined,
+}
+
+impl TradeReducerRevisionStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::None => "none",
+ Self::Proposed => "proposed",
+ Self::Accepted => "accepted",
+ Self::Declined => "declined",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeRevisionStatus {
+ #[default]
+ None,
+ ChangeProposed,
+ Updated,
+ KeptAsPlaced,
+}
+
+impl TradeRevisionStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::None => "none",
+ Self::ChangeProposed => "change_proposed",
+ Self::Updated => "updated",
+ Self::KeptAsPlaced => "kept_as_placed",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::None => "messages.trade.workflow.revision.none",
+ Self::ChangeProposed => "messages.trade.workflow.revision.change_proposed",
+ Self::Updated => "messages.trade.workflow.revision.updated",
+ Self::KeptAsPlaced => "messages.trade.workflow.revision.kept_as_placed",
+ }
+ }
+
+ pub const fn from_reducer_status(status: TradeReducerRevisionStatus) -> Self {
+ match status {
+ TradeReducerRevisionStatus::None => Self::None,
+ TradeReducerRevisionStatus::Proposed => Self::ChangeProposed,
+ TradeReducerRevisionStatus::Accepted => Self::Updated,
+ TradeReducerRevisionStatus::Declined => Self::KeptAsPlaced,
+ }
+ }
+}
+
+impl From<TradeReducerRevisionStatus> for TradeRevisionStatus {
+ fn from(status: TradeReducerRevisionStatus) -> Self {
+ Self::from_reducer_status(status)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeReducerFulfillmentStatus {
+ AcceptedNotFulfilled,
+ Preparing,
+ ReadyForPickup,
+ OutForDelivery,
+ Delivered,
+ SellerCancelled,
+}
+
+impl TradeReducerFulfillmentStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::AcceptedNotFulfilled => "accepted_not_fulfilled",
+ Self::Preparing => "preparing",
+ Self::ReadyForPickup => "ready_for_pickup",
+ Self::OutForDelivery => "out_for_delivery",
+ Self::Delivered => "delivered",
+ Self::SellerCancelled => "seller_cancelled",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeFulfillmentStatus {
+ #[default]
+ Confirmed,
+ Preparing,
+ ReadyForPickup,
+ OutForDelivery,
+ Delivered,
+ Cancelled,
+}
+
+impl TradeFulfillmentStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::Confirmed => "confirmed",
+ Self::Preparing => "preparing",
+ Self::ReadyForPickup => "ready_for_pickup",
+ Self::OutForDelivery => "out_for_delivery",
+ Self::Delivered => "delivered",
+ Self::Cancelled => "cancelled",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::Confirmed => "messages.trade.workflow.fulfillment.confirmed",
+ Self::Preparing => "messages.trade.workflow.fulfillment.preparing",
+ Self::ReadyForPickup => "messages.trade.workflow.fulfillment.ready_for_pickup",
+ Self::OutForDelivery => "messages.trade.workflow.fulfillment.out_for_delivery",
+ Self::Delivered => "messages.trade.workflow.fulfillment.delivered",
+ Self::Cancelled => "messages.trade.workflow.fulfillment.cancelled",
+ }
+ }
+
+ pub const fn from_reducer_status(status: TradeReducerFulfillmentStatus) -> Self {
+ match status {
+ TradeReducerFulfillmentStatus::AcceptedNotFulfilled => Self::Confirmed,
+ TradeReducerFulfillmentStatus::Preparing => Self::Preparing,
+ TradeReducerFulfillmentStatus::ReadyForPickup => Self::ReadyForPickup,
+ TradeReducerFulfillmentStatus::OutForDelivery => Self::OutForDelivery,
+ TradeReducerFulfillmentStatus::Delivered => Self::Delivered,
+ TradeReducerFulfillmentStatus::SellerCancelled => Self::Cancelled,
+ }
+ }
+}
+
+impl From<TradeReducerFulfillmentStatus> for TradeFulfillmentStatus {
+ fn from(status: TradeReducerFulfillmentStatus) -> Self {
+ Self::from_reducer_status(status)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeInventoryStatus {
+ Available,
+ Reserved,
+ SoldOut,
+ #[default]
+ NeedsReview,
+}
+
+impl TradeInventoryStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::Available => "available",
+ Self::Reserved => "reserved",
+ Self::SoldOut => "sold_out",
+ Self::NeedsReview => "needs_review",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::Available => "messages.trade.workflow.inventory.available",
+ Self::Reserved => "messages.trade.workflow.inventory.reserved",
+ Self::SoldOut => "messages.trade.workflow.inventory.sold_out",
+ Self::NeedsReview => "messages.trade.workflow.inventory.needs_review",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradePaymentDisplayStatus {
+ #[default]
+ NotRecorded,
+ Recorded,
+ NeedsReview,
+}
+
+impl TradePaymentDisplayStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::NotRecorded => "not_recorded",
+ Self::Recorded => "recorded",
+ Self::NeedsReview => "needs_review",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::NotRecorded => "messages.trade.workflow.payment.not_recorded",
+ Self::Recorded => "messages.trade.workflow.payment.recorded",
+ Self::NeedsReview => "messages.trade.workflow.payment.needs_review",
+ }
+ }
+
+ pub const fn allows_payment_action(self) -> bool {
+ false
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeWorkflowSource {
+ App,
+ Cli,
+ Relay,
+ LocalEvents,
+ #[default]
+ Unknown,
+}
+
+impl TradeWorkflowSource {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::App => "app",
+ Self::Cli => "cli",
+ Self::Relay => "relay",
+ Self::LocalEvents => "local_events",
+ Self::Unknown => "unknown",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::App => "messages.trade.workflow.provenance.app",
+ Self::Cli => "messages.trade.workflow.provenance.cli",
+ Self::Relay => "messages.trade.workflow.provenance.relay",
+ Self::LocalEvents => "messages.trade.workflow.provenance.local_events",
+ Self::Unknown => "messages.trade.workflow.provenance.unknown",
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+pub struct TradeEconomicsProjection {
+ pub subtotal_minor_units: Option<u32>,
+ pub discount_total_minor_units: Option<u32>,
+ pub adjustment_total_minor_units: Option<u32>,
+ pub total_minor_units: Option<u32>,
+ pub currency_code: Option<String>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+pub struct TradeProvenanceProjection {
+ pub primary_source: TradeWorkflowSource,
+ pub sources: BTreeSet<TradeWorkflowSource>,
+ pub last_event_id: Option<String>,
+}
+
+impl TradeProvenanceProjection {
+ pub fn new(
+ primary_source: TradeWorkflowSource,
+ sources: impl IntoIterator<Item = TradeWorkflowSource>,
+ ) -> Self {
+ let mut sources = sources.into_iter().collect::<BTreeSet<_>>();
+ sources.insert(primary_source);
+ Self {
+ primary_source,
+ sources,
+ last_event_id: None,
+ }
+ }
+
+ pub fn from_primary_source(primary_source: TradeWorkflowSource) -> Self {
+ Self::new(primary_source, [primary_source])
+ }
+}
+
+impl Default for TradeProvenanceProjection {
+ fn default() -> Self {
+ Self::from_primary_source(TradeWorkflowSource::Unknown)
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+pub struct TradeWorkflowProjection {
+ pub order_id: OrderId,
+ pub agreement: TradeAgreementStatus,
+ pub revision: TradeRevisionStatus,
+ pub fulfillment: Option<TradeFulfillmentStatus>,
+ pub economics: TradeEconomicsProjection,
+ pub inventory: TradeInventoryStatus,
+ pub payment: TradePaymentDisplayStatus,
+ pub provenance: TradeProvenanceProjection,
+}
+
+impl TradeWorkflowProjection {
+ pub fn new(order_id: OrderId, agreement: TradeAgreementStatus) -> Self {
+ Self {
+ order_id,
+ agreement,
+ revision: TradeRevisionStatus::None,
+ fulfillment: None,
+ economics: TradeEconomicsProjection::default(),
+ inventory: TradeInventoryStatus::NeedsReview,
+ payment: TradePaymentDisplayStatus::NotRecorded,
+ provenance: TradeProvenanceProjection::default(),
+ }
+ }
+
+ pub fn from_reducer_status(order_id: OrderId, agreement: TradeReducerAgreementStatus) -> Self {
+ Self::new(
+ order_id,
+ TradeAgreementStatus::from_reducer_status(agreement),
+ )
+ }
+}
+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrdersFilter {
@@ -1641,6 +2033,10 @@ mod tests {
SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection,
ShellSection, StartupSignerEntryProjection, StartupSignerSource, StartupSignerSourceKind,
TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary,
+ TradeAgreementStatus, TradeFulfillmentStatus, TradeInventoryStatus,
+ TradePaymentDisplayStatus, TradeProvenanceProjection, TradeReducerAgreementStatus,
+ TradeReducerFulfillmentStatus, TradeReducerRevisionStatus, TradeRevisionStatus,
+ TradeWorkflowProjection, TradeWorkflowSource,
};
use std::{collections::BTreeSet, str::FromStr};
use uuid::Uuid;
@@ -2250,6 +2646,130 @@ mod tests {
}
#[test]
+ fn trade_workflow_projection_maps_reducer_status_to_product_axes() {
+ assert_eq!(
+ TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Requested),
+ TradeAgreementStatus::Ordered
+ );
+ assert_eq!(
+ TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Accepted),
+ TradeAgreementStatus::Confirmed
+ );
+ assert_eq!(
+ TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Disputed),
+ TradeAgreementStatus::NeedsReview
+ );
+ assert_eq!(
+ TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Invalid),
+ TradeAgreementStatus::NeedsReview
+ );
+ assert_eq!(
+ TradeFulfillmentStatus::from_reducer_status(
+ TradeReducerFulfillmentStatus::AcceptedNotFulfilled
+ ),
+ TradeFulfillmentStatus::Confirmed
+ );
+ assert_eq!(
+ TradeFulfillmentStatus::from_reducer_status(
+ TradeReducerFulfillmentStatus::SellerCancelled
+ ),
+ TradeFulfillmentStatus::Cancelled
+ );
+ assert_eq!(
+ TradeRevisionStatus::from_reducer_status(TradeReducerRevisionStatus::Proposed),
+ TradeRevisionStatus::ChangeProposed
+ );
+ assert_eq!(
+ TradeRevisionStatus::from_reducer_status(TradeReducerRevisionStatus::Accepted),
+ TradeRevisionStatus::Updated
+ );
+ assert_eq!(
+ TradeRevisionStatus::from_reducer_status(TradeReducerRevisionStatus::Declined),
+ TradeRevisionStatus::KeptAsPlaced
+ );
+
+ let order_id = OrderId::new();
+ let projection = TradeWorkflowProjection::from_reducer_status(
+ order_id,
+ TradeReducerAgreementStatus::Requested,
+ );
+ assert_eq!(projection.order_id, order_id);
+ assert_eq!(projection.agreement, TradeAgreementStatus::Ordered);
+ assert_eq!(projection.revision, TradeRevisionStatus::None);
+ assert_eq!(projection.fulfillment, None);
+ assert_eq!(projection.inventory, TradeInventoryStatus::NeedsReview);
+ assert_eq!(projection.payment, TradePaymentDisplayStatus::NotRecorded);
+ assert!(!projection.payment.allows_payment_action());
+ assert_eq!(
+ projection.provenance,
+ TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::Unknown)
+ );
+ }
+
+ #[test]
+ fn trade_workflow_projection_uses_localization_key_ids_for_visible_status_labels() {
+ assert_eq!(
+ TradeReducerAgreementStatus::Requested.storage_key(),
+ "requested"
+ );
+ assert_eq!(
+ TradeReducerFulfillmentStatus::AcceptedNotFulfilled.storage_key(),
+ "accepted_not_fulfilled"
+ );
+ assert_eq!(
+ TradeReducerRevisionStatus::Proposed.storage_key(),
+ "proposed"
+ );
+ assert_eq!(TradeAgreementStatus::Ordered.storage_key(), "ordered");
+ assert_eq!(
+ TradeFulfillmentStatus::ReadyForPickup.storage_key(),
+ "ready_for_pickup"
+ );
+ assert_eq!(
+ TradeRevisionStatus::KeptAsPlaced.storage_key(),
+ "kept_as_placed"
+ );
+ assert_eq!(TradeInventoryStatus::Reserved.storage_key(), "reserved");
+ assert_eq!(
+ TradePaymentDisplayStatus::NotRecorded.storage_key(),
+ "not_recorded"
+ );
+ assert_eq!(
+ TradeWorkflowSource::LocalEvents.storage_key(),
+ "local_events"
+ );
+
+ assert_eq!(
+ TradeAgreementStatus::Ordered.label_key_id(),
+ "messages.trade.workflow.agreement.ordered"
+ );
+ assert_eq!(
+ TradeAgreementStatus::NeedsReview.label_key_id(),
+ "messages.trade.workflow.agreement.needs_review"
+ );
+ assert_eq!(
+ TradeRevisionStatus::ChangeProposed.label_key_id(),
+ "messages.trade.workflow.revision.change_proposed"
+ );
+ assert_eq!(
+ TradeFulfillmentStatus::ReadyForPickup.label_key_id(),
+ "messages.trade.workflow.fulfillment.ready_for_pickup"
+ );
+ assert_eq!(
+ TradeInventoryStatus::SoldOut.label_key_id(),
+ "messages.trade.workflow.inventory.sold_out"
+ );
+ assert_eq!(
+ TradePaymentDisplayStatus::NotRecorded.label_key_id(),
+ "messages.trade.workflow.payment.not_recorded"
+ );
+ assert_eq!(
+ TradeWorkflowSource::Cli.label_key_id(),
+ "messages.trade.workflow.provenance.cli"
+ );
+ }
+
+ #[test]
fn orders_and_pack_day_query_state_defaults_are_frozen() {
assert_eq!(
OrdersScreenQueryState::default(),
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -133,11 +133,11 @@
"personal.orders.status.completed": "Completed",
"personal.orders.status.declined": "Declined",
"personal.orders.status.refunded": "Refunded",
- "personal.cart.surface.body": "Review items from one farm and continue to checkout when you're ready.",
+ "personal.cart.surface.body": "Review items from one farm before placing the order.",
"personal.order_summary.title": "Order summary",
"personal.fulfillment.title": "Fulfillment",
"personal.cart.remove_line.action": "Remove",
- "personal.cart.continue_checkout.action": "Continue to checkout",
+ "personal.cart.continue_checkout.action": "Review order",
"personal.cart.line.quantity.label": "Quantity",
"personal.cart.line.unit_price.label": "Unit price",
"personal.cart.line.total.label": "Line total",
@@ -151,14 +151,14 @@
"personal.detail.replace_cart.body": "is already in your cart. Replace it with items from",
"personal.detail.replace_cart.action": "Replace cart",
"personal.detail.keep_current_cart.action": "Keep current cart",
- "personal.checkout.title": "Checkout",
+ "personal.checkout.title": "Order review",
"personal.checkout.back_action": "Back to cart",
"personal.checkout.contact.title": "Contact",
"personal.checkout.field.name": "Name",
"personal.checkout.field.email": "Email",
"personal.checkout.field.phone": "Phone",
"personal.checkout.field.order_note": "Order note",
- "personal.checkout.local_only.body": "This places a local order on this device. It does not charge a card.",
+ "personal.checkout.local_only.body": "Review the details before placing the order.",
"personal.checkout.place_order.action": "Place order",
"orders.title": "Orders",
"orders.filters.title": "View",
@@ -193,8 +193,8 @@
"orders.recovery.section.title": "Recovery",
"orders.recovery.missed_pickup.title": "Missed pickup",
"orders.recovery.missed_pickup.body": "Use this when a buyer did not collect the order as planned.",
- "orders.recovery.refund_follow_up.title": "Refund follow-up",
- "orders.recovery.refund_follow_up.body": "Track a refund conversation here. Payment handling stays outside the app.",
+ "orders.recovery.refund_follow_up.title": "Payment status",
+ "orders.recovery.refund_follow_up.body": "Track the recorded payment state for this order.",
"orders.recovery.last_updated.label": "Last updated",
"orders.recovery.action.open_follow_up": "Open follow-up",
"orders.recovery.action.start_review": "Start review",
@@ -203,6 +203,34 @@
"orders.recovery.state.open": "Open",
"orders.recovery.state.in_review": "In review",
"orders.recovery.state.resolved": "Resolved",
+ "trade.workflow.agreement.ordered": "Ordered",
+ "trade.workflow.agreement.confirmed": "Confirmed",
+ "trade.workflow.agreement.declined": "Declined",
+ "trade.workflow.agreement.cancelled": "Cancelled",
+ "trade.workflow.agreement.completed": "Completed",
+ "trade.workflow.agreement.needs_review": "Needs review",
+ "trade.workflow.revision.none": "No change",
+ "trade.workflow.revision.change_proposed": "Change proposed",
+ "trade.workflow.revision.updated": "Updated",
+ "trade.workflow.revision.kept_as_placed": "Kept as placed",
+ "trade.workflow.fulfillment.confirmed": "Confirmed",
+ "trade.workflow.fulfillment.preparing": "Preparing",
+ "trade.workflow.fulfillment.ready_for_pickup": "Ready for pickup",
+ "trade.workflow.fulfillment.out_for_delivery": "Out for delivery",
+ "trade.workflow.fulfillment.delivered": "Delivered",
+ "trade.workflow.fulfillment.cancelled": "Cancelled",
+ "trade.workflow.inventory.available": "Available",
+ "trade.workflow.inventory.reserved": "Reserved",
+ "trade.workflow.inventory.sold_out": "Sold out",
+ "trade.workflow.inventory.needs_review": "Needs review",
+ "trade.workflow.payment.not_recorded": "Not recorded",
+ "trade.workflow.payment.recorded": "Recorded",
+ "trade.workflow.payment.needs_review": "Needs review",
+ "trade.workflow.provenance.app": "App",
+ "trade.workflow.provenance.cli": "CLI",
+ "trade.workflow.provenance.relay": "Relay",
+ "trade.workflow.provenance.local_events": "Local events",
+ "trade.workflow.provenance.unknown": "Unknown",
"orders.reminders.title": "Reminders",
"orders.reminder_log.title": "Reminder activity",
"orders.reminder_log.empty.body": "Recent reminder activity appears here after something needs attention.",