app

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

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:
Mcrates/i18n/src/keys.rs | 28++++++++++++++++++++++++++++
Mcrates/i18n/src/lib.rs | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/view/src/lib.rs | 520+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mi18n/locales/en/messages.json | 40++++++++++++++++++++++++++++++++++------
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.",