commit 36bbbfe808fc7b9043daa27e98ec6f4a548c13ba
parent 280ffcdcf3b5f301db4a5127f94855e6e6d93bf7
Author: triesap <tyson@radroots.org>
Date: Wed, 3 Jun 2026 17:36:49 -0700
buyer: align order review workflow
Diffstat:
13 files changed, 921 insertions(+), 354 deletions(-)
diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs
@@ -44,7 +44,7 @@ use radroots_app_sync::{
use radroots_app_view::{
ActiveSurface, AppActivityContext, AppActivityKind, AppIdentityProjection, AppStartupGate,
BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection,
- BuyerCheckoutDraft, BuyerContext, BuyerOrderDetailProjection, BuyerOrderStatus,
+ BuyerContext, BuyerOrderDetailProjection, BuyerOrderReviewDraft, BuyerOrderStatus,
BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmProfileRecord, FarmReadiness,
FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection,
FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId,
@@ -636,11 +636,12 @@ impl DesktopAppRuntime {
self.lock_state_mut().remove_personal_cart_line(product_id)
}
- pub fn save_personal_checkout_draft(
+ pub fn save_personal_order_review_draft(
&self,
- draft: BuyerCheckoutDraft,
+ draft: BuyerOrderReviewDraft,
) -> Result<bool, AppSqliteError> {
- self.lock_state_mut().save_personal_checkout_draft(draft)
+ self.lock_state_mut()
+ .save_personal_order_review_draft(draft)
}
pub fn place_personal_order(&self) -> Result<bool, AppSqliteError> {
@@ -1751,15 +1752,15 @@ impl DesktopAppRuntimeState {
let next_cart = next_buyer_cart_for_detail(current_cart, &detail, replace_existing)?;
sqlite_store.replace_buyer_cart(&buyer_context, &next_cart)?;
let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
- let refreshed_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
+ let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
let cart_changed = self.mutate_personal_projection(|projection| {
let mut changed = false;
if projection.cart.cart != refreshed_cart {
projection.cart.cart = refreshed_cart.clone();
changed = true;
}
- if projection.cart.checkout != refreshed_checkout {
- projection.cart.checkout = refreshed_checkout.clone();
+ if projection.cart.order_review != refreshed_order_review {
+ projection.cart.order_review = refreshed_order_review.clone();
changed = true;
}
changed
@@ -1797,28 +1798,28 @@ impl DesktopAppRuntimeState {
}
let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
- let refreshed_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
+ let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
- Ok(self.refresh_personal_cart_and_checkout(refreshed_cart, refreshed_checkout))
+ Ok(self.refresh_personal_cart_and_order_review(refreshed_cart, refreshed_order_review))
}
- fn save_personal_checkout_draft(
+ fn save_personal_order_review_draft(
&mut self,
- draft: BuyerCheckoutDraft,
+ draft: BuyerOrderReviewDraft,
) -> Result<bool, AppSqliteError> {
let Some(sqlite_store) = self.sqlite_store.as_ref() else {
return Ok(false);
};
let buyer_context = self.state_store.identity_projection().buyer_context();
- sqlite_store.save_buyer_checkout_draft(&buyer_context, &draft)?;
- let refreshed_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
+ sqlite_store.save_buyer_order_review_draft(&buyer_context, &draft)?;
+ let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
Ok(self.mutate_personal_projection(|projection| {
- if projection.cart.checkout == refreshed_checkout {
+ if projection.cart.order_review == refreshed_order_review {
return false;
}
- projection.cart.checkout = refreshed_checkout;
+ projection.cart.order_review = refreshed_order_review;
true
}))
}
@@ -1827,16 +1828,16 @@ impl DesktopAppRuntimeState {
let buyer_context = self.state_store.identity_projection().buyer_context();
if matches!(buyer_context, BuyerContext::Guest) {
return Err(AppSqliteError::InvalidProjection {
- reason: "buyer checkout requires a selected account",
+ reason: "buyer order review requires a selected account",
});
}
- let (refreshed_cart, refreshed_checkout, refreshed_orders, order_detail, order_export) = {
+ let (refreshed_cart, refreshed_order_review, refreshed_orders, order_detail, order_export) = {
let Some(sqlite_store) = self.sqlite_store.as_ref() else {
return Ok(false);
};
let order_id = sqlite_store.place_buyer_order(&buyer_context)?;
let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
- let refreshed_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
+ let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
let refreshed_orders = sqlite_store.load_buyer_orders(&buyer_context)?;
if !refreshed_orders
.rows
@@ -1863,7 +1864,7 @@ impl DesktopAppRuntimeState {
};
(
refreshed_cart,
- refreshed_checkout,
+ refreshed_order_review,
refreshed_orders,
order_detail,
order_export,
@@ -1875,8 +1876,8 @@ impl DesktopAppRuntimeState {
projection.cart.cart = refreshed_cart.clone();
changed = true;
}
- if projection.cart.checkout != refreshed_checkout {
- projection.cart.checkout = refreshed_checkout.clone();
+ if projection.cart.order_review != refreshed_order_review {
+ projection.cart.order_review = refreshed_order_review.clone();
changed = true;
}
if projection.orders.list != refreshed_orders {
@@ -2022,7 +2023,7 @@ impl DesktopAppRuntimeState {
.map(|detail| detail.order_id);
let (
refreshed_cart,
- refreshed_checkout,
+ refreshed_order_review,
refreshed_orders,
refreshed_order_detail,
has_recoverable_coordination,
@@ -2031,7 +2032,7 @@ impl DesktopAppRuntimeState {
return Ok(false);
};
let refreshed_cart = sqlite_store.load_buyer_cart(buyer_context)?;
- let refreshed_checkout = sqlite_store.load_buyer_checkout(buyer_context)?;
+ let refreshed_order_review = sqlite_store.load_buyer_order_review(buyer_context)?;
let refreshed_orders = sqlite_store.load_buyer_orders(buyer_context)?;
let has_recoverable_coordination = !sqlite_store
.load_recoverable_buyer_order_coordination_records(buyer_context)?
@@ -2057,7 +2058,7 @@ impl DesktopAppRuntimeState {
};
(
refreshed_cart,
- refreshed_checkout,
+ refreshed_order_review,
refreshed_orders,
refreshed_order_detail,
has_recoverable_coordination,
@@ -2070,8 +2071,8 @@ impl DesktopAppRuntimeState {
projection.cart.cart = refreshed_cart.clone();
changed = true;
}
- if projection.cart.checkout != refreshed_checkout {
- projection.cart.checkout = refreshed_checkout.clone();
+ if projection.cart.order_review != refreshed_order_review {
+ projection.cart.order_review = refreshed_order_review.clone();
changed = true;
}
if projection.orders.list != refreshed_orders {
@@ -2124,7 +2125,8 @@ impl DesktopAppRuntimeState {
)? {
BuyerRepeatDemandApplyOutcome::Applied => {
let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
- let refreshed_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
+ let refreshed_order_review =
+ sqlite_store.load_buyer_order_review(&buyer_context)?;
let refreshed_orders = sqlite_store.load_buyer_orders(&buyer_context)?;
let refreshed_detail =
sqlite_store.load_buyer_order_detail(&buyer_context, order_id)?;
@@ -2134,8 +2136,8 @@ impl DesktopAppRuntimeState {
projection.cart.cart = refreshed_cart.clone();
changed = true;
}
- if projection.cart.checkout != refreshed_checkout {
- projection.cart.checkout = refreshed_checkout.clone();
+ if projection.cart.order_review != refreshed_order_review {
+ projection.cart.order_review = refreshed_order_review.clone();
changed = true;
}
if projection.orders.list != refreshed_orders {
@@ -5136,10 +5138,10 @@ impl DesktopAppRuntimeState {
sqlite_store.load_buyer_listings(&query.search_query, &query.fulfillment_methods)
}
- fn refresh_personal_cart_and_checkout(
+ fn refresh_personal_cart_and_order_review(
&mut self,
refreshed_cart: BuyerCartProjection,
- refreshed_checkout: radroots_app_view::BuyerCheckoutProjection,
+ refreshed_order_review: radroots_app_view::BuyerOrderReviewProjection,
) -> bool {
self.mutate_personal_projection(|projection| {
let mut changed = false;
@@ -5147,8 +5149,8 @@ impl DesktopAppRuntimeState {
projection.cart.cart = refreshed_cart.clone();
changed = true;
}
- if projection.cart.checkout != refreshed_checkout {
- projection.cart.checkout = refreshed_checkout.clone();
+ if projection.cart.order_review != refreshed_order_review {
+ projection.cart.order_review = refreshed_order_review.clone();
changed = true;
}
@@ -7985,7 +7987,7 @@ fn load_selected_account_context_with_options(
None => None,
};
let buyer_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
- let buyer_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
+ let buyer_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
let buyer_orders = sqlite_store.load_buyer_orders(&buyer_context)?;
let has_recoverable_coordination = !sqlite_store
.load_recoverable_buyer_order_coordination_records(&buyer_context)?
@@ -8006,7 +8008,7 @@ fn load_selected_account_context_with_options(
},
cart: BuyerCartScreenProjection {
cart: buyer_cart,
- checkout: buyer_checkout,
+ order_review: buyer_order_review,
},
orders: BuyerOrdersScreenProjection {
list: buyer_orders,
@@ -9615,20 +9617,21 @@ mod tests {
use radroots_app_view::{
AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface,
AppActivityKind, AppIdentityProjection, AppStartupGate, BlackoutPeriodId,
- BlackoutPeriodRecord, BuyerCheckoutDisabledReason, BuyerCheckoutDraft, 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, PackDayPrintFailureKind, PackDayPrintKind,
- PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow,
- PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductId,
- ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsSort, RecoveryKind,
- RecoveryRecordId, ReminderDeliveryState, ReminderFeedProjection, ReminderKind,
- SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection,
- ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary,
+ BlackoutPeriodRecord, BuyerOrderReviewDisabledReason, BuyerOrderReviewDraft,
+ 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,
+ PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow,
+ PackDayProjection, PackDayRosterRow, PersonalSection, PickupLocationId,
+ PickupLocationRecord, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductStatus,
+ ProductsFilter, ProductsSort, RecoveryKind, RecoveryRecordId, ReminderDeliveryState,
+ ReminderFeedProjection, ReminderKind, SelectedAccountProjection, SelectedSurfaceProjection,
+ SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask,
+ TodaySetupTaskKind, TodaySummary,
};
use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
@@ -14275,7 +14278,7 @@ mod tests {
}
#[test]
- fn runtime_removing_buyer_cart_line_clears_cart_and_checkout_readiness() {
+ fn runtime_removing_buyer_cart_line_clears_cart_and_order_review_readiness() {
let runtime = memory_runtime();
let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
assert!(
@@ -14331,9 +14334,20 @@ mod tests {
let summary = runtime.summary();
assert!(summary.personal_projection.cart.cart.lines.is_empty());
assert!(summary.personal_projection.cart.cart.farm_id.is_none());
- assert!(!summary.personal_projection.cart.checkout.can_place_order);
+ assert!(
+ !summary
+ .personal_projection
+ .cart
+ .order_review
+ .can_place_order
+ );
assert_eq!(
- summary.personal_projection.cart.checkout.summary.line_count,
+ summary
+ .personal_projection
+ .cart
+ .order_review
+ .summary
+ .line_count,
0
);
}
@@ -14387,17 +14401,17 @@ mod tests {
);
assert!(
runtime
- .save_personal_checkout_draft(BuyerCheckoutDraft {
+ .save_personal_order_review_draft(BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: "555-0101".to_owned(),
order_note: "Leave by the cooler".to_owned(),
})
- .expect("buyer checkout draft should save")
+ .expect("buyer order review draft should save")
);
- let checkout = runtime.summary().personal_projection.cart.checkout;
- assert!(checkout.can_place_order);
- assert_eq!(checkout.place_order_disabled_reason, None);
+ let order_review = runtime.summary().personal_projection.cart.order_review;
+ assert!(order_review.can_place_order);
+ assert_eq!(order_review.place_order_disabled_reason, None);
assert!(
runtime
.place_personal_order()
@@ -14410,14 +14424,20 @@ mod tests {
ShellSection::Personal(PersonalSection::Orders)
);
assert!(summary.personal_projection.cart.cart.lines.is_empty());
- assert!(!summary.personal_projection.cart.checkout.can_place_order);
+ assert!(
+ !summary
+ .personal_projection
+ .cart
+ .order_review
+ .can_place_order
+ );
assert_eq!(
summary
.personal_projection
.cart
- .checkout
+ .order_review
.place_order_disabled_reason,
- Some(BuyerCheckoutDisabledReason::EmptyCart)
+ Some(BuyerOrderReviewDisabledReason::EmptyCart)
);
assert_eq!(summary.personal_projection.orders.list.rows.len(), 1);
assert_eq!(
@@ -14454,7 +14474,7 @@ mod tests {
}
#[test]
- fn runtime_guest_checkout_requires_account_before_order_write() {
+ fn runtime_guest_order_review_requires_account_before_order_write() {
let runtime = memory_runtime();
let farm_id = FarmId::new();
runtime
@@ -14513,13 +14533,13 @@ mod tests {
);
assert!(
runtime
- .save_personal_checkout_draft(BuyerCheckoutDraft {
+ .save_personal_order_review_draft(BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: "555-0101".to_owned(),
order_note: "Leave by the cooler".to_owned(),
})
- .expect("buyer checkout draft should save")
+ .expect("buyer order review draft should save")
);
let ready_summary = runtime.summary();
@@ -14527,22 +14547,22 @@ mod tests {
!ready_summary
.personal_projection
.cart
- .checkout
+ .order_review
.can_place_order
);
assert_eq!(
ready_summary
.personal_projection
.cart
- .checkout
+ .order_review
.place_order_disabled_reason,
- Some(BuyerCheckoutDisabledReason::AccountRequired)
+ Some(BuyerOrderReviewDisabledReason::AccountRequired)
);
assert_eq!(
ready_summary
.personal_projection
.cart
- .checkout
+ .order_review
.summary
.line_count,
1
@@ -14550,7 +14570,7 @@ mod tests {
let error = runtime
.place_personal_order()
- .expect_err("guest checkout should require an account");
+ .expect_err("guest order review should require an account");
assert!(matches!(error, AppSqliteError::InvalidProjection { .. }));
let summary = runtime.summary();
@@ -14982,13 +15002,13 @@ mod tests {
.expect("listing projection should mutate after cart snapshot");
assert!(
runtime
- .save_personal_checkout_draft(BuyerCheckoutDraft {
+ .save_personal_order_review_draft(BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: "555-0101".to_owned(),
order_note: "Leave by the cooler".to_owned(),
})
- .expect("buyer checkout draft should save")
+ .expect("buyer order review draft should save")
);
assert!(
runtime
@@ -15433,13 +15453,13 @@ mod tests {
);
assert!(
runtime
- .save_personal_checkout_draft(BuyerCheckoutDraft {
+ .save_personal_order_review_draft(BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: String::new(),
order_note: String::new(),
})
- .expect("buyer checkout draft should save")
+ .expect("buyer order review draft should save")
);
assert!(
runtime
@@ -15545,13 +15565,13 @@ mod tests {
);
assert!(
runtime
- .save_personal_checkout_draft(BuyerCheckoutDraft {
+ .save_personal_order_review_draft(BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: String::new(),
order_note: String::new(),
})
- .expect("buyer checkout draft should save")
+ .expect("buyer order review draft should save")
);
assert!(
runtime
@@ -19515,13 +19535,13 @@ mod tests {
);
assert!(
runtime
- .save_personal_checkout_draft(BuyerCheckoutDraft {
+ .save_personal_order_review_draft(BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: String::new(),
order_note: String::new(),
})
- .expect("buyer checkout draft should save")
+ .expect("buyer order review draft should save")
);
block_shared_local_events_database(&paths);
@@ -19542,7 +19562,13 @@ mod tests {
.has_recoverable_coordination
);
assert!(summary.personal_projection.cart.cart.lines.is_empty());
- assert!(!summary.personal_projection.cart.checkout.can_place_order);
+ assert!(
+ !summary
+ .personal_projection
+ .cart
+ .order_review
+ .can_place_order
+ );
assert_eq!(summary.personal_projection.orders.list.rows.len(), 1);
let visible_order_id = summary.personal_projection.orders.list.rows[0].order_id;
let order_detail = summary
diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs
@@ -45,10 +45,10 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"buyer-detail-keep-current",
"buyer-detail-quantity-decrease",
"buyer-detail-quantity-increase",
- "buyer-cart-open-checkout",
+ "buyer-cart-open-order-review",
"buyer-cart-remove-line",
- "buyer-checkout-back",
- "buyer-checkout-place-order",
+ "buyer-order-review-back",
+ "buyer-order-review-place-order",
"buyer-listing-open",
"buyer-order-accept-change",
"buyer-order-confirm-replace",
@@ -61,8 +61,8 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"personal_orders",
"buyer.add_to_cart_failed",
"buyer.cart_remove_failed",
- "buyer.checkout_place_failed",
- "buyer.checkout_save_failed",
+ "buyer.order_review_place_failed",
+ "buyer.order_review_save_failed",
"buyer.detail_open_failed",
"buyer.order_open_failed",
"buyer.order_cancel_failed",
@@ -88,7 +88,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"failed to retry buyer order coordination",
"failed to remove buyer cart line",
"failed to reorder buyer order",
- "failed to save buyer checkout draft",
+ "failed to save buyer order review draft",
"failed to select buyer section",
"failed to open buyer product detail",
"failed to update buyer fulfillment filter",
@@ -423,7 +423,7 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::PersonalOrderSummaryTitle",
"AppTextKey::PersonalFulfillmentTitle",
"AppTextKey::PersonalCartRemoveLineAction",
- "AppTextKey::PersonalCartContinueCheckoutAction",
+ "AppTextKey::PersonalCartReviewOrderAction",
"AppTextKey::PersonalCartLineQuantityLabel",
"AppTextKey::PersonalCartLineUnitPriceLabel",
"AppTextKey::PersonalCartLineTotalLabel",
@@ -437,15 +437,15 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::PersonalDetailReplaceCartBody",
"AppTextKey::PersonalDetailReplaceCartAction",
"AppTextKey::PersonalDetailKeepCurrentCartAction",
- "AppTextKey::PersonalCheckoutTitle",
- "AppTextKey::PersonalCheckoutBackAction",
- "AppTextKey::PersonalCheckoutContactTitle",
- "AppTextKey::PersonalCheckoutFieldName",
- "AppTextKey::PersonalCheckoutFieldEmail",
- "AppTextKey::PersonalCheckoutFieldPhone",
- "AppTextKey::PersonalCheckoutFieldOrderNote",
- "AppTextKey::PersonalCheckoutLocalOnlyBody",
- "AppTextKey::PersonalCheckoutPlaceOrderAction",
+ "AppTextKey::PersonalOrderReviewTitle",
+ "AppTextKey::PersonalOrderReviewBackAction",
+ "AppTextKey::PersonalOrderReviewContactTitle",
+ "AppTextKey::PersonalOrderReviewFieldName",
+ "AppTextKey::PersonalOrderReviewFieldEmail",
+ "AppTextKey::PersonalOrderReviewFieldPhone",
+ "AppTextKey::PersonalOrderReviewFieldOrderNote",
+ "AppTextKey::PersonalOrderReviewLocalOnlyBody",
+ "AppTextKey::PersonalOrderReviewPlaceOrderAction",
"AppTextKey::HomeTodayOpenInOrdersAction",
"AppTextKey::HomeTodayOpenInPackDayAction",
"AppTextKey::OrdersTitle",
diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs
@@ -48,8 +48,8 @@ use radroots_app_ui::{
pub use radroots_app_view::SettingsSection as SettingsPanelViewKey;
use radroots_app_view::{
AppStartupGate, BlackoutPeriodId, BlackoutPeriodRecord, BuyerCartProjection,
- BuyerCartReplaceConfirmationProjection, BuyerCheckoutDraft, BuyerCheckoutSummaryProjection,
- BuyerListingRow, BuyerOrderDetailProjection, BuyerOrderStatus, BuyerOrdersListRow,
+ BuyerCartReplaceConfirmationProjection, BuyerListingRow, BuyerOrderDetailProjection,
+ BuyerOrderReviewDraft, BuyerOrderReviewSummaryProjection, BuyerOrderStatus, BuyerOrdersListRow,
BuyerProductDetailProjection, FarmId, FarmOperatingRulesRecord, FarmOrderMethod,
FarmProfileRecord, FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness,
FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection,
@@ -220,7 +220,7 @@ pub struct HomeView {
startup_signer_recovery_attempted: bool,
farm_setup_form: Option<FarmSetupFormState>,
personal_search: Option<PersonalSearchState>,
- buyer_checkout_form: Option<BuyerCheckoutFormState>,
+ buyer_order_review_form: Option<BuyerOrderReviewFormState>,
products_search: Option<ProductsSearchState>,
products_stock_editor: Option<ProductsStockEditorState>,
product_editor_form: Option<ProductEditorFormState>,
@@ -263,7 +263,7 @@ struct HomeAutoFocusState {
startup_signer_input_is_editable: bool,
has_farm_setup_form: bool,
has_personal_search_input: bool,
- has_buyer_checkout_form: bool,
+ has_buyer_order_review_form: bool,
has_products_search_input: bool,
has_products_stock_editor: bool,
has_product_editor_form: bool,
@@ -278,8 +278,8 @@ enum HomeAutoFocusTarget {
BuyerSearchInput,
BuyerListingOpenFirst,
BuyerDetailBack,
- BuyerCartOpenCheckout,
- BuyerCheckoutNameInput,
+ BuyerCartOpenOrderReview,
+ BuyerOrderReviewNameInput,
BuyerOrderOpenFirst,
BuyerOrderConfirmReplace,
BuyerOrderRepeatDemand,
@@ -336,7 +336,7 @@ impl HomeView {
startup_signer_recovery_attempted: false,
farm_setup_form: None,
personal_search: None,
- buyer_checkout_form: None,
+ buyer_order_review_form: None,
products_search: None,
products_stock_editor: None,
product_editor_form: None,
@@ -353,7 +353,7 @@ impl HomeView {
),
has_farm_setup_form: self.farm_setup_form.is_some(),
has_personal_search_input: self.personal_search.is_some(),
- has_buyer_checkout_form: self.buyer_checkout_form.is_some(),
+ has_buyer_order_review_form: self.buyer_order_review_form.is_some(),
has_products_search_input: self.products_search.is_some(),
has_products_stock_editor: self.products_stock_editor.is_some(),
has_product_editor_form: self.product_editor_form.is_some(),
@@ -404,11 +404,11 @@ impl HomeView {
HomeAutoFocusTarget::BuyerDetailBack => {
focus_button(window, "buyer-detail-back", cx);
}
- HomeAutoFocusTarget::BuyerCartOpenCheckout => {
- focus_button(window, "buyer-cart-open-checkout", cx);
+ HomeAutoFocusTarget::BuyerCartOpenOrderReview => {
+ focus_button(window, "buyer-cart-open-order-review", cx);
}
- HomeAutoFocusTarget::BuyerCheckoutNameInput => {
- if let Some(form) = self.buyer_checkout_form.as_ref() {
+ HomeAutoFocusTarget::BuyerOrderReviewNameInput => {
+ if let Some(form) = self.buyer_order_review_form.as_ref() {
form.name_input
.update(cx, |input, cx| input.focus(window, cx));
}
@@ -1029,7 +1029,7 @@ impl HomeView {
}
}
- fn sync_buyer_checkout_form(
+ fn sync_buyer_order_review_form(
&mut self,
runtime_summary: &DesktopAppRuntimeSummary,
window: &mut Window,
@@ -1044,25 +1044,29 @@ impl HomeView {
.lines
.is_empty()
{
- self.buyer_checkout_form = None;
+ self.buyer_order_review_form = None;
return;
}
let workspace_id = personal_workspace_id(runtime_summary);
- let draft = &runtime_summary.personal_projection.cart.checkout.draft;
+ let draft = &runtime_summary.personal_projection.cart.order_review.draft;
let should_reset = self
- .buyer_checkout_form
+ .buyer_order_review_form
.as_ref()
.map(|form| form.workspace_id != workspace_id)
.unwrap_or(false);
if should_reset {
- self.buyer_checkout_form =
- Some(BuyerCheckoutFormState::new(workspace_id, draft, window, cx));
+ self.buyer_order_review_form = Some(BuyerOrderReviewFormState::new(
+ workspace_id,
+ draft,
+ window,
+ cx,
+ ));
return;
}
- if let Some(form) = self.buyer_checkout_form.as_mut() {
+ if let Some(form) = self.buyer_order_review_form.as_mut() {
form.sync(draft, window, cx);
}
}
@@ -1323,7 +1327,7 @@ impl HomeView {
}
}
- fn handle_buyer_checkout_input_event(
+ fn handle_buyer_order_review_input_event(
&mut self,
state: &Entity<InputState>,
event: &InputEvent,
@@ -1334,7 +1338,7 @@ impl HomeView {
return;
}
- let Some(form) = self.buyer_checkout_form.as_ref() else {
+ let Some(form) = self.buyer_order_review_form.as_ref() else {
return;
};
let matches_input = form.name_input == *state
@@ -1347,16 +1351,16 @@ impl HomeView {
match self
.runtime
- .save_personal_checkout_draft(form.current_draft(cx))
+ .save_personal_order_review_draft(form.current_draft(cx))
{
Ok(true) => cx.notify(),
Ok(false) => {}
Err(runtime_error) => {
error!(
target: "buyer",
- event = "buyer.checkout_save_failed",
+ event = "buyer.order_review_save_failed",
error = %runtime_error,
- "failed to save buyer checkout draft"
+ "failed to save buyer order review draft"
);
}
}
@@ -1499,8 +1503,8 @@ impl HomeView {
}
}
- fn open_personal_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- if self.buyer_checkout_form.is_some() {
+ fn open_personal_order_review(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if self.buyer_order_review_form.is_some() {
return;
}
@@ -1517,17 +1521,17 @@ impl HomeView {
return;
}
- self.buyer_checkout_form = Some(BuyerCheckoutFormState::new(
+ self.buyer_order_review_form = Some(BuyerOrderReviewFormState::new(
personal_workspace_id(&runtime_summary),
- &runtime_summary.personal_projection.cart.checkout.draft,
+ &runtime_summary.personal_projection.cart.order_review.draft,
window,
cx,
));
cx.notify();
}
- fn close_personal_checkout(&mut self, cx: &mut Context<Self>) {
- if self.buyer_checkout_form.take().is_some() {
+ fn close_personal_order_review(&mut self, cx: &mut Context<Self>) {
+ if self.buyer_order_review_form.take().is_some() {
cx.notify();
}
}
@@ -1557,7 +1561,7 @@ impl HomeView {
fn place_personal_order_update(&mut self) -> bool {
match self.runtime.place_personal_order() {
Ok(true) => {
- self.buyer_checkout_form = None;
+ self.buyer_order_review_form = None;
let _ = self.clear_buyer_workspace_notice();
true
}
@@ -1565,11 +1569,11 @@ impl HomeView {
Err(runtime_error) => {
let notice = buyer_order_place_failure_notice(&runtime_error);
if notice == BuyerWorkspaceNotice::OrderCoordinationFailed {
- self.buyer_checkout_form = None;
+ self.buyer_order_review_form = None;
}
error!(
target: "buyer",
- event = "buyer.checkout_place_failed",
+ event = "buyer.order_review_place_failed",
error = %runtime_error,
"failed to place buyer order"
);
@@ -3190,7 +3194,7 @@ impl HomeView {
cx: &mut Context<Self>,
) -> AnyElement {
let cart = &runtime.personal_projection.cart.cart;
- let checkout = &runtime.personal_projection.cart.checkout;
+ let order_review = &runtime.personal_projection.cart.order_review;
app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
.w_full()
@@ -3210,15 +3214,15 @@ impl HomeView {
.w_full()
.child(buyer_cart_card(
cart,
- &checkout.summary,
- self.buyer_checkout_form.is_some(),
+ &order_review.summary,
+ self.buyer_order_review_form.is_some(),
cx,
))
- .when_some(self.buyer_checkout_form.as_ref(), |this, form| {
- this.child(buyer_checkout_card(
+ .when_some(self.buyer_order_review_form.as_ref(), |this, form| {
+ this.child(buyer_order_review_card(
form,
- checkout,
- cx.listener(|this, _, _, cx| this.close_personal_checkout(cx)),
+ order_review,
+ cx.listener(|this, _, _, cx| this.close_personal_order_review(cx)),
cx.listener(|this, _, _, cx| this.place_personal_order(cx)),
cx,
))
@@ -4573,7 +4577,7 @@ impl Render for HomeView {
self.sync_startup_signer_entry(&runtime_summary, window, cx);
self.sync_farm_setup_form(&runtime_summary, window, cx);
self.sync_personal_search(&runtime_summary, window, cx);
- self.sync_buyer_checkout_form(&runtime_summary, window, cx);
+ self.sync_buyer_order_review_form(&runtime_summary, window, cx);
self.sync_products_search(&runtime_summary, window, cx);
self.sync_products_stock_editor(&runtime_summary);
self.sync_product_editor_form(&runtime_summary, window, cx);
@@ -4698,7 +4702,7 @@ impl PersonalSearchState {
}
}
-struct BuyerCheckoutFormState {
+struct BuyerOrderReviewFormState {
workspace_id: String,
name_input: Entity<InputState>,
email_input: Entity<InputState>,
@@ -4710,10 +4714,10 @@ struct BuyerCheckoutFormState {
_order_note_subscription: Subscription,
}
-impl BuyerCheckoutFormState {
+impl BuyerOrderReviewFormState {
fn new(
workspace_id: String,
- draft: &BuyerCheckoutDraft,
+ draft: &BuyerOrderReviewDraft,
window: &mut Window,
cx: &mut Context<HomeView>,
) -> Self {
@@ -4727,22 +4731,22 @@ impl BuyerCheckoutFormState {
let name_subscription = cx.subscribe_in(
&name_input,
window,
- HomeView::handle_buyer_checkout_input_event,
+ HomeView::handle_buyer_order_review_input_event,
);
let email_subscription = cx.subscribe_in(
&email_input,
window,
- HomeView::handle_buyer_checkout_input_event,
+ HomeView::handle_buyer_order_review_input_event,
);
let phone_subscription = cx.subscribe_in(
&phone_input,
window,
- HomeView::handle_buyer_checkout_input_event,
+ HomeView::handle_buyer_order_review_input_event,
);
let order_note_subscription = cx.subscribe_in(
&order_note_input,
window,
- HomeView::handle_buyer_checkout_input_event,
+ HomeView::handle_buyer_order_review_input_event,
);
Self {
@@ -4760,14 +4764,14 @@ impl BuyerCheckoutFormState {
fn sync(
&mut self,
- draft: &BuyerCheckoutDraft,
+ draft: &BuyerOrderReviewDraft,
window: &mut Window,
cx: &mut Context<HomeView>,
) {
- sync_checkout_input(&self.name_input, draft.name.as_str(), window, cx);
- sync_checkout_input(&self.email_input, draft.email.as_str(), window, cx);
- sync_checkout_input(&self.phone_input, draft.phone.as_str(), window, cx);
- sync_checkout_input(
+ sync_order_review_input(&self.name_input, draft.name.as_str(), window, cx);
+ sync_order_review_input(&self.email_input, draft.email.as_str(), window, cx);
+ sync_order_review_input(&self.phone_input, draft.phone.as_str(), window, cx);
+ sync_order_review_input(
&self.order_note_input,
draft.order_note.as_str(),
window,
@@ -4775,8 +4779,8 @@ impl BuyerCheckoutFormState {
);
}
- fn current_draft(&self, cx: &App) -> BuyerCheckoutDraft {
- BuyerCheckoutDraft {
+ fn current_draft(&self, cx: &App) -> BuyerOrderReviewDraft {
+ BuyerOrderReviewDraft {
name: self.name_input.read(cx).value().to_string(),
email: self.email_input.read(cx).value().to_string(),
phone: self.phone_input.read(cx).value().to_string(),
@@ -4785,7 +4789,7 @@ impl BuyerCheckoutFormState {
}
}
-fn sync_checkout_input(
+fn sync_order_review_input(
input: &Entity<InputState>,
value: &str,
window: &mut Window,
@@ -7607,10 +7611,10 @@ fn buyer_auto_focus_target(
}
}
PersonalSection::Cart => {
- if state.has_buyer_checkout_form {
- Some(HomeAutoFocusTarget::BuyerCheckoutNameInput)
+ if state.has_buyer_order_review_form {
+ Some(HomeAutoFocusTarget::BuyerOrderReviewNameInput)
} else if !runtime.personal_projection.cart.cart.lines.is_empty() {
- Some(HomeAutoFocusTarget::BuyerCartOpenCheckout)
+ Some(HomeAutoFocusTarget::BuyerCartOpenOrderReview)
} else {
None
}
@@ -8410,8 +8414,8 @@ fn buyer_product_detail_card(
fn buyer_cart_card(
cart: &BuyerCartProjection,
- summary: &BuyerCheckoutSummaryProjection,
- checkout_open: bool,
+ summary: &BuyerOrderReviewSummaryProjection,
+ order_review_open: bool,
cx: &mut Context<HomeView>,
) -> impl IntoElement {
app_surface_card(
@@ -8433,11 +8437,11 @@ fn buyer_cart_card(
)))
.child(label_value_list(buyer_order_summary_rows(summary))),
))
- .when(!checkout_open, |this| {
+ .when(!order_review_open, |this| {
this.child(action_button_primary(
- "buyer-cart-open-checkout",
- app_shared_text(AppTextKey::PersonalCartContinueCheckoutAction),
- cx.listener(|this, _, window, cx| this.open_personal_checkout(window, cx)),
+ "buyer-cart-open-order-review",
+ app_shared_text(AppTextKey::PersonalCartReviewOrderAction),
+ cx.listener(|this, _, window, cx| this.open_personal_order_review(window, cx)),
cx,
))
}),
@@ -8498,9 +8502,9 @@ fn buyer_cart_line_card(
)
}
-fn buyer_checkout_card(
- form: &BuyerCheckoutFormState,
- checkout: &radroots_app_view::BuyerCheckoutProjection,
+fn buyer_order_review_card(
+ form: &BuyerOrderReviewFormState,
+ order_review: &radroots_app_view::BuyerOrderReviewProjection,
on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
on_place_order: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
cx: &App,
@@ -8516,17 +8520,17 @@ fn buyer_checkout_card(
.justify_between()
.gap(px(APP_UI_THEME.shells.home_stack_gap_px))
.child(app_text_value(app_shared_text(
- AppTextKey::PersonalCheckoutTitle,
+ AppTextKey::PersonalOrderReviewTitle,
)))
.child(text_button(
- "buyer-checkout-back",
- app_shared_text(AppTextKey::PersonalCheckoutBackAction),
+ "buyer-order-review-back",
+ app_shared_text(AppTextKey::PersonalOrderReviewBackAction),
on_close,
cx,
)),
)
.child(home_body_text(app_shared_text(
- AppTextKey::PersonalCheckoutLocalOnlyBody,
+ AppTextKey::PersonalOrderReviewLocalOnlyBody,
)))
.child(app_surface_panel(
app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
@@ -8536,7 +8540,7 @@ fn buyer_checkout_card(
AppTextKey::PersonalOrderSummaryTitle,
)))
.child(label_value_list(buyer_order_summary_rows(
- &checkout.summary,
+ &order_review.summary,
))),
))
.child(app_surface_panel(
@@ -8547,7 +8551,7 @@ fn buyer_checkout_card(
AppTextKey::PersonalFulfillmentTitle,
)))
.child(home_body_text(
- checkout
+ order_review
.summary
.fulfillment_summary
.clone()
@@ -8555,7 +8559,7 @@ fn buyer_checkout_card(
)),
))
.child(app_form_section(
- app_shared_text(AppTextKey::PersonalCheckoutContactTitle),
+ app_shared_text(AppTextKey::PersonalOrderReviewContactTitle),
div()
.w_full()
.flex()
@@ -8563,7 +8567,7 @@ fn buyer_checkout_card(
.gap(px(APP_UI_THEME.shells.home_stack_gap_px))
.child(app_form_input_text(
AppFormFieldSpec::new(
- app_shared_text(AppTextKey::PersonalCheckoutFieldName),
+ app_shared_text(AppTextKey::PersonalOrderReviewFieldName),
Option::<SharedString>::None,
),
&form.name_input,
@@ -8571,7 +8575,7 @@ fn buyer_checkout_card(
))
.child(app_form_input_text(
AppFormFieldSpec::new(
- app_shared_text(AppTextKey::PersonalCheckoutFieldEmail),
+ app_shared_text(AppTextKey::PersonalOrderReviewFieldEmail),
Option::<SharedString>::None,
),
&form.email_input,
@@ -8579,7 +8583,7 @@ fn buyer_checkout_card(
))
.child(app_form_input_text(
AppFormFieldSpec::new(
- app_shared_text(AppTextKey::PersonalCheckoutFieldPhone),
+ app_shared_text(AppTextKey::PersonalOrderReviewFieldPhone),
Option::<SharedString>::None,
),
&form.phone_input,
@@ -8587,25 +8591,25 @@ fn buyer_checkout_card(
))
.child(app_form_input_text(
AppFormFieldSpec::new(
- app_shared_text(AppTextKey::PersonalCheckoutFieldOrderNote),
+ app_shared_text(AppTextKey::PersonalOrderReviewFieldOrderNote),
Option::<SharedString>::None,
),
&form.order_note_input,
false,
)),
))
- .child(if checkout.can_place_order {
+ .child(if order_review.can_place_order {
action_button_primary(
- "buyer-checkout-place-order",
- app_shared_text(AppTextKey::PersonalCheckoutPlaceOrderAction),
+ "buyer-order-review-place-order",
+ app_shared_text(AppTextKey::PersonalOrderReviewPlaceOrderAction),
on_place_order,
cx,
)
.into_any_element()
} else {
action_button_primary_disabled(
- "buyer-checkout-place-order",
- app_shared_text(AppTextKey::PersonalCheckoutPlaceOrderAction),
+ "buyer-order-review-place-order",
+ app_shared_text(AppTextKey::PersonalOrderReviewPlaceOrderAction),
cx,
)
.into_any_element()
@@ -8613,7 +8617,7 @@ fn buyer_checkout_card(
)
}
-fn buyer_order_summary_rows(summary: &BuyerCheckoutSummaryProjection) -> Vec<LabelValueRow> {
+fn buyer_order_summary_rows(summary: &BuyerOrderReviewSummaryProjection) -> Vec<LabelValueRow> {
vec![
LabelValueRow::new(
app_shared_text(AppTextKey::PersonalSummaryFarmLabel),
@@ -8891,7 +8895,10 @@ fn buyer_orders_list_entry(
.flex_1()
.min_w_0()
.child(app_text_label(row.order_number.clone()))
- .child(settings_badge_text(row.farm_display_name.clone())),
+ .child(settings_badge_text(row.farm_display_name.clone()))
+ .child(settings_badge_text(trade_economics_total_text(
+ &row.workflow.economics,
+ ))),
)
.child(
div()
@@ -13812,20 +13819,20 @@ mod tests {
Some(HomeAutoFocusTarget::BuyerSearchInput)
);
- let mut buyer_cart_checkout = buyer_search.clone();
- buyer_cart_checkout.shell_projection = AppShellProjection::new(
+ let mut buyer_cart_order_review = buyer_search.clone();
+ buyer_cart_order_review.shell_projection = AppShellProjection::new(
ActiveSurface::Personal,
ShellSection::Personal(PersonalSection::Cart),
);
assert_eq!(
home_auto_focus_target(
- &buyer_cart_checkout,
+ &buyer_cart_order_review,
HomeAutoFocusState {
- has_buyer_checkout_form: true,
+ has_buyer_order_review_form: true,
..HomeAutoFocusState::default()
},
),
- Some(HomeAutoFocusTarget::BuyerCheckoutNameInput)
+ Some(HomeAutoFocusTarget::BuyerOrderReviewNameInput)
);
let order_id = OrderId::new();
diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs
@@ -163,7 +163,7 @@ define_app_text_keys! {
PersonalOrderSummaryTitle => "personal.order_summary.title",
PersonalFulfillmentTitle => "personal.fulfillment.title",
PersonalCartRemoveLineAction => "personal.cart.remove_line.action",
- PersonalCartContinueCheckoutAction => "personal.cart.continue_checkout.action",
+ PersonalCartReviewOrderAction => "personal.cart.review_order.action",
PersonalCartLineQuantityLabel => "personal.cart.line.quantity.label",
PersonalCartLineUnitPriceLabel => "personal.cart.line.unit_price.label",
PersonalCartLineTotalLabel => "personal.cart.line.total.label",
@@ -177,15 +177,15 @@ define_app_text_keys! {
PersonalDetailReplaceCartBody => "personal.detail.replace_cart.body",
PersonalDetailReplaceCartAction => "personal.detail.replace_cart.action",
PersonalDetailKeepCurrentCartAction => "personal.detail.keep_current_cart.action",
- PersonalCheckoutTitle => "personal.checkout.title",
- PersonalCheckoutBackAction => "personal.checkout.back_action",
- PersonalCheckoutContactTitle => "personal.checkout.contact.title",
- PersonalCheckoutFieldName => "personal.checkout.field.name",
- PersonalCheckoutFieldEmail => "personal.checkout.field.email",
- PersonalCheckoutFieldPhone => "personal.checkout.field.phone",
- PersonalCheckoutFieldOrderNote => "personal.checkout.field.order_note",
- PersonalCheckoutLocalOnlyBody => "personal.checkout.local_only.body",
- PersonalCheckoutPlaceOrderAction => "personal.checkout.place_order.action",
+ PersonalOrderReviewTitle => "personal.order_review.title",
+ PersonalOrderReviewBackAction => "personal.order_review.back_action",
+ PersonalOrderReviewContactTitle => "personal.order_review.contact.title",
+ PersonalOrderReviewFieldName => "personal.order_review.field.name",
+ PersonalOrderReviewFieldEmail => "personal.order_review.field.email",
+ PersonalOrderReviewFieldPhone => "personal.order_review.field.phone",
+ PersonalOrderReviewFieldOrderNote => "personal.order_review.field.order_note",
+ PersonalOrderReviewLocalOnlyBody => "personal.order_review.local_only.body",
+ PersonalOrderReviewPlaceOrderAction => "personal.order_review.place_order.action",
OrdersTitle => "orders.title",
OrdersFiltersTitle => "orders.filters.title",
OrdersSummaryTotal => "orders.summary.total",
diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs
@@ -353,18 +353,21 @@ mod tests {
}
#[test]
- fn english_marketplace_checkout_copy_matches_the_local_order_contract() {
+ fn english_marketplace_order_review_copy_matches_the_local_order_contract() {
assert_eq!(
- app_text(AppTextKey::PersonalCartContinueCheckoutAction),
+ app_text(AppTextKey::PersonalCartReviewOrderAction),
"Review order"
);
- assert_eq!(app_text(AppTextKey::PersonalCheckoutTitle), "Order review");
assert_eq!(
- app_text(AppTextKey::PersonalCheckoutPlaceOrderAction),
+ app_text(AppTextKey::PersonalOrderReviewTitle),
+ "Order review"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrderReviewPlaceOrderAction),
"Place order"
);
assert_eq!(
- app_text(AppTextKey::PersonalCheckoutLocalOnlyBody),
+ app_text(AppTextKey::PersonalOrderReviewLocalOnlyBody),
"Review the details before placing the order."
);
assert_eq!(
@@ -380,9 +383,9 @@ mod tests {
#[test]
fn english_payment_action_copy_remains_unspoken_for_reserved_workflow() {
let action_keys = [
- AppTextKey::PersonalCartContinueCheckoutAction,
- AppTextKey::PersonalCheckoutBackAction,
- AppTextKey::PersonalCheckoutPlaceOrderAction,
+ AppTextKey::PersonalCartReviewOrderAction,
+ AppTextKey::PersonalOrderReviewBackAction,
+ AppTextKey::PersonalOrderReviewPlaceOrderAction,
AppTextKey::OrdersRecoveryActionOpenFollowUp,
AppTextKey::OrdersRecoveryActionStartReview,
AppTextKey::OrdersRecoveryActionMarkOpen,
@@ -398,6 +401,9 @@ mod tests {
"bank",
"card",
"processor",
+ "provider",
+ "payment-provider",
+ "payment provider",
];
for key in action_keys {
@@ -408,7 +414,7 @@ mod tests {
}
for copy in [
- app_text(AppTextKey::PersonalCheckoutLocalOnlyBody),
+ app_text(AppTextKey::PersonalOrderReviewLocalOnlyBody),
app_text(AppTextKey::PersonalOrderCoordinationFailedNotice),
] {
let normalized_copy = copy.to_lowercase();
diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs
@@ -13,7 +13,7 @@ use radroots_app_sync::{
};
use radroots_app_view::{
ActiveSurface, AppIdentityProjection, AppStartupGate, BuyerCartProjection,
- BuyerCheckoutProjection, BuyerListingsProjection, BuyerOrderDetailProjection,
+ BuyerListingsProjection, BuyerOrderDetailProjection, BuyerOrderReviewProjection,
BuyerOrdersProjection, BuyerProductDetailProjection, FarmOrderMethod, FarmReadiness,
FarmReadinessBlocker, FarmRulesProjection, FarmSetupBlocker, FarmSetupProjection,
FarmSetupReadiness, FarmTimingConflict, FulfillmentWindowId, LoggedOutStartupPhase,
@@ -125,7 +125,7 @@ pub struct BuyerSearchScreenProjection {
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct BuyerCartScreenProjection {
pub cart: BuyerCartProjection,
- pub checkout: BuyerCheckoutProjection,
+ pub order_review: BuyerOrderReviewProjection,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
diff --git a/crates/store/migrations/0023_order_workflow_display_projection.sql b/crates/store/migrations/0023_order_workflow_display_projection.sql
@@ -0,0 +1,56 @@
+ALTER TABLE orders
+ ADD COLUMN workflow_agreement TEXT NOT NULL DEFAULT 'ordered' CHECK (
+ workflow_agreement IN ('ordered', 'confirmed', 'declined', 'cancelled', 'completed', 'needs_review')
+ );
+
+ALTER TABLE orders
+ ADD COLUMN workflow_fulfillment TEXT CHECK (
+ workflow_fulfillment IS NULL OR workflow_fulfillment IN ('confirmed', 'preparing', 'ready_for_pickup', 'out_for_delivery', 'delivered', 'cancelled')
+ );
+
+ALTER TABLE orders
+ ADD COLUMN workflow_inventory TEXT NOT NULL DEFAULT 'needs_review' CHECK (
+ workflow_inventory IN ('available', 'reserved', 'sold_out', 'needs_review')
+ );
+
+ALTER TABLE orders
+ ADD COLUMN workflow_payment TEXT NOT NULL DEFAULT 'not_recorded' CHECK (
+ workflow_payment IN ('not_recorded', 'recorded', 'needs_review')
+ );
+
+ALTER TABLE orders
+ ADD COLUMN workflow_provenance_source TEXT NOT NULL DEFAULT 'unknown' CHECK (
+ workflow_provenance_source IN ('app', 'cli', 'relay', 'local_events', 'unknown')
+ );
+
+ALTER TABLE orders
+ ADD COLUMN workflow_provenance_last_event_id TEXT;
+
+UPDATE orders
+SET
+ workflow_agreement = CASE status
+ WHEN 'scheduled' THEN 'confirmed'
+ WHEN 'packed' THEN 'confirmed'
+ WHEN 'completed' THEN 'completed'
+ WHEN 'declined' THEN 'declined'
+ WHEN 'refunded' THEN 'needs_review'
+ ELSE 'ordered'
+ END,
+ workflow_fulfillment = CASE status
+ WHEN 'scheduled' THEN 'confirmed'
+ WHEN 'packed' THEN 'ready_for_pickup'
+ WHEN 'completed' THEN 'delivered'
+ WHEN 'declined' THEN 'cancelled'
+ ELSE NULL
+ END,
+ workflow_inventory = CASE status
+ WHEN 'scheduled' THEN 'reserved'
+ WHEN 'packed' THEN 'reserved'
+ WHEN 'completed' THEN 'reserved'
+ WHEN 'declined' THEN 'available'
+ ELSE 'needs_review'
+ END,
+ workflow_payment = CASE status
+ WHEN 'refunded' THEN 'needs_review'
+ ELSE 'not_recorded'
+ END;
diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs
@@ -11,7 +11,7 @@ use radroots_events::{
kinds::{
KIND_TRADE_CANCEL, KIND_TRADE_FULFILLMENT_UPDATE, KIND_TRADE_ORDER_REQUEST,
KIND_TRADE_ORDER_RESPONSE, KIND_TRADE_ORDER_REVISION, KIND_TRADE_ORDER_REVISION_RESPONSE,
- KIND_TRADE_RECEIPT,
+ KIND_TRADE_PAYMENT_RECORDED, KIND_TRADE_RECEIPT, KIND_TRADE_SETTLEMENT_DECISION,
},
trade::{
RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested,
@@ -23,7 +23,8 @@ use radroots_events_codec::trade::{
active_trade_fulfillment_update_from_event, active_trade_order_cancel_from_event,
active_trade_order_decision_from_event, active_trade_order_request_from_event,
active_trade_order_revision_decision_from_event,
- active_trade_order_revision_proposal_from_event,
+ active_trade_order_revision_proposal_from_event, active_trade_payment_recorded_from_event,
+ active_trade_settlement_decision_from_event,
};
use radroots_local_events::{
LocalEventRecord, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus,
@@ -32,9 +33,10 @@ use radroots_local_events::{
use radroots_sql_core::{SqlExecutor, SqliteExecutor};
use radroots_trade::order::{
RadrootsActiveOrderCancellationRecord, RadrootsActiveOrderDecisionRecord,
- RadrootsActiveOrderFulfillmentRecord, RadrootsActiveOrderProjection,
- RadrootsActiveOrderReceiptRecord, RadrootsActiveOrderRequestRecord,
- RadrootsActiveOrderRevisionDecisionRecord, RadrootsActiveOrderRevisionProposalRecord,
+ RadrootsActiveOrderFulfillmentRecord, RadrootsActiveOrderPaymentRecord,
+ RadrootsActiveOrderProjection, RadrootsActiveOrderReceiptRecord,
+ RadrootsActiveOrderRequestRecord, RadrootsActiveOrderRevisionDecisionRecord,
+ RadrootsActiveOrderRevisionProposalRecord, RadrootsActiveOrderSettlementRecord,
reduce_active_order_events,
};
use rusqlite::{Connection, OptionalExtension, params};
@@ -56,7 +58,9 @@ const KIND_ORDER_REVISION_DECISION: i64 = KIND_TRADE_ORDER_REVISION_RESPONSE as
const KIND_ORDER_CANCEL: i64 = KIND_TRADE_CANCEL as i64;
const KIND_ORDER_FULFILLMENT: i64 = KIND_TRADE_FULFILLMENT_UPDATE as i64;
const KIND_ORDER_RECEIPT: i64 = KIND_TRADE_RECEIPT as i64;
-const ACTIVE_ORDER_EVENT_KINDS: [i64; 7] = [
+const KIND_ORDER_PAYMENT: i64 = KIND_TRADE_PAYMENT_RECORDED as i64;
+const KIND_ORDER_SETTLEMENT: i64 = KIND_TRADE_SETTLEMENT_DECISION as i64;
+const ACTIVE_ORDER_EVENT_KINDS: [i64; 9] = [
KIND_ORDER_REQUEST,
KIND_ORDER_DECISION,
KIND_ORDER_REVISION,
@@ -64,6 +68,8 @@ const ACTIVE_ORDER_EVENT_KINDS: [i64; 7] = [
KIND_ORDER_CANCEL,
KIND_ORDER_FULFILLMENT,
KIND_ORDER_RECEIPT,
+ KIND_ORDER_PAYMENT,
+ KIND_ORDER_SETTLEMENT,
];
#[derive(Clone, Debug, Default, Eq, PartialEq)]
@@ -1091,8 +1097,8 @@ impl<'a> AppLocalInteropRepository<'a> {
buckets.fulfillments,
buckets.cancellations,
buckets.receipts,
- [],
- [],
+ buckets.payments,
+ buckets.settlements,
);
let request_payload = projection.request_event_id.as_deref().and_then(|event_id| {
requests
@@ -1196,12 +1202,26 @@ impl<'a> AppLocalInteropRepository<'a> {
"UPDATE orders
SET status = ?2,
workflow_revision = ?3,
+ workflow_agreement = ?4,
+ workflow_fulfillment = ?5,
+ workflow_inventory = ?6,
+ workflow_payment = ?7,
+ workflow_provenance_source = ?8,
+ workflow_provenance_last_event_id = ?9,
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = ?1",
params![
workflow.order_id.to_string(),
status.storage_key(),
- workflow.revision.storage_key()
+ workflow.revision.storage_key(),
+ workflow.agreement.storage_key(),
+ workflow
+ .fulfillment
+ .map(|fulfillment| fulfillment.storage_key()),
+ workflow.inventory.storage_key(),
+ workflow.payment.storage_key(),
+ workflow.provenance.primary_source.storage_key(),
+ workflow.provenance.last_event_id.as_deref()
],
)
.map_err(|source| AppSqliteError::Query {
@@ -2456,6 +2476,8 @@ enum ActiveOrderEvidence {
Fulfillment(RadrootsActiveOrderFulfillmentRecord),
Cancellation(RadrootsActiveOrderCancellationRecord),
Receipt(RadrootsActiveOrderReceiptRecord),
+ Payment(RadrootsActiveOrderPaymentRecord),
+ Settlement(RadrootsActiveOrderSettlementRecord),
}
impl ActiveOrderEvidence {
@@ -2468,6 +2490,8 @@ impl ActiveOrderEvidence {
Self::Fulfillment(record) => record.event_id.as_str(),
Self::Cancellation(record) => record.event_id.as_str(),
Self::Receipt(record) => record.event_id.as_str(),
+ Self::Payment(record) => record.event_id.as_str(),
+ Self::Settlement(record) => record.event_id.as_str(),
}
}
@@ -2480,6 +2504,8 @@ impl ActiveOrderEvidence {
Self::Fulfillment(record) => record.payload.order_id.as_str(),
Self::Cancellation(record) => record.payload.order_id.as_str(),
Self::Receipt(record) => record.payload.order_id.as_str(),
+ Self::Payment(record) => record.payload.order_id.as_str(),
+ Self::Settlement(record) => record.payload.order_id.as_str(),
}
}
@@ -2491,7 +2517,9 @@ impl ActiveOrderEvidence {
| Self::RevisionDecision(_)
| Self::Fulfillment(_)
| Self::Cancellation(_)
- | Self::Receipt(_) => None,
+ | Self::Receipt(_)
+ | Self::Payment(_)
+ | Self::Settlement(_) => None,
}
}
@@ -2525,6 +2553,14 @@ impl ActiveOrderEvidence {
record.payload.order_id.as_str(),
record.payload.buyer_pubkey.as_str(),
),
+ Self::Payment(record) => (
+ record.payload.order_id.as_str(),
+ record.payload.buyer_pubkey.as_str(),
+ ),
+ Self::Settlement(record) => (
+ record.payload.order_id.as_str(),
+ record.payload.buyer_pubkey.as_str(),
+ ),
}
}
}
@@ -2538,6 +2574,8 @@ struct ActiveOrderEvidenceBuckets {
fulfillments: Vec<RadrootsActiveOrderFulfillmentRecord>,
cancellations: Vec<RadrootsActiveOrderCancellationRecord>,
receipts: Vec<RadrootsActiveOrderReceiptRecord>,
+ payments: Vec<RadrootsActiveOrderPaymentRecord>,
+ settlements: Vec<RadrootsActiveOrderSettlementRecord>,
}
impl ActiveOrderEvidenceBuckets {
@@ -2556,6 +2594,8 @@ impl ActiveOrderEvidenceBuckets {
ActiveOrderEvidence::Fulfillment(record) => buckets.fulfillments.push(record),
ActiveOrderEvidence::Cancellation(record) => buckets.cancellations.push(record),
ActiveOrderEvidence::Receipt(record) => buckets.receipts.push(record),
+ ActiveOrderEvidence::Payment(record) => buckets.payments.push(record),
+ ActiveOrderEvidence::Settlement(record) => buckets.settlements.push(record),
}
}
buckets
@@ -2812,6 +2852,36 @@ fn active_order_evidence_from_event(event: &RadrootsNostrEvent) -> Option<Active
},
))
}
+ KIND_ORDER_PAYMENT => {
+ let envelope = active_trade_payment_recorded_from_event(event).ok()?;
+ let context =
+ active_trade_event_context_from_tags(envelope.message_type, &event.tags).ok()?;
+ Some(ActiveOrderEvidence::Payment(
+ RadrootsActiveOrderPaymentRecord {
+ event_id: event.id.clone(),
+ author_pubkey: event.author.clone(),
+ counterparty_pubkey: context.counterparty_pubkey,
+ root_event_id: context.root_event_id?,
+ prev_event_id: context.prev_event_id?,
+ payload: envelope.payload,
+ },
+ ))
+ }
+ KIND_ORDER_SETTLEMENT => {
+ let envelope = active_trade_settlement_decision_from_event(event).ok()?;
+ let context =
+ active_trade_event_context_from_tags(envelope.message_type, &event.tags).ok()?;
+ Some(ActiveOrderEvidence::Settlement(
+ RadrootsActiveOrderSettlementRecord {
+ event_id: event.id.clone(),
+ author_pubkey: event.author.clone(),
+ counterparty_pubkey: context.counterparty_pubkey,
+ root_event_id: context.root_event_id?,
+ prev_event_id: context.prev_event_id?,
+ payload: envelope.payload,
+ },
+ ))
+ }
_ => None,
}
}
@@ -3562,7 +3632,8 @@ mod tests {
use radroots_app_view::{
BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderStatus, OrdersFilter,
- OrdersScreenQueryState, ProductAvailabilityState, ProductId, TradeRevisionStatus,
+ OrdersScreenQueryState, ProductAvailabilityState, ProductId, TradePaymentDisplayStatus,
+ TradeRevisionStatus, TradeWorkflowSource,
};
use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
@@ -3577,7 +3648,7 @@ mod tests {
RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem,
RadrootsTradeOrderRequested, RadrootsTradeOrderRevisionDecision,
RadrootsTradeOrderRevisionDecisionEvent, RadrootsTradeOrderRevisionProposed,
- RadrootsTradePricingBasis,
+ RadrootsTradePaymentMethod, RadrootsTradePaymentRecorded, RadrootsTradePricingBasis,
},
};
use radroots_events_codec::{
@@ -3587,6 +3658,7 @@ mod tests {
active_trade_order_request_event_build,
active_trade_order_revision_decision_event_build,
active_trade_order_revision_proposal_event_build,
+ active_trade_payment_recorded_event_build,
},
wire::WireEventParts,
};
@@ -3595,6 +3667,7 @@ mod tests {
LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime,
};
use radroots_sql_core::SqliteExecutor;
+ use radroots_trade::order::radroots_trade_order_economics_digest;
use rusqlite::params;
use serde_json::json;
use uuid::Uuid;
@@ -4242,6 +4315,32 @@ mod tests {
}
}
+ fn payment_recorded_payload(
+ request: &RadrootsTradeOrderRequested,
+ root_event_id: &str,
+ previous_event_id: &str,
+ agreement_event_id: &str,
+ ) -> RadrootsTradePaymentRecorded {
+ RadrootsTradePaymentRecorded {
+ order_id: request.order_id.clone(),
+ listing_addr: request.listing_addr.clone(),
+ buyer_pubkey: request.buyer_pubkey.clone(),
+ seller_pubkey: request.seller_pubkey.clone(),
+ root_event_id: root_event_id.to_owned(),
+ previous_event_id: previous_event_id.to_owned(),
+ agreement_event_id: agreement_event_id.to_owned(),
+ quote_id: request.economics.quote_id.clone(),
+ quote_version: request.economics.quote_version,
+ economics_digest: radroots_trade_order_economics_digest(&request.economics)
+ .expect("order economics digest should encode"),
+ amount: request.economics.total.amount,
+ currency: request.economics.total.currency,
+ method: RadrootsTradePaymentMethod::ManualTransfer,
+ reference: Some("manual reference".to_owned()),
+ paid_at: Some(1_777_665_800),
+ }
+ }
+
fn event_from_parts(event_id: &str, author: &str, parts: WireEventParts) -> RadrootsNostrEvent {
RadrootsNostrEvent {
id: event_id.to_owned(),
@@ -4671,8 +4770,68 @@ mod tests {
assert_eq!(decision_report.imported_records, 1);
assert_eq!(buyer_orders.rows.len(), 1);
assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Scheduled);
+ assert_eq!(
+ buyer_orders.rows[0].workflow.payment,
+ TradePaymentDisplayStatus::NotRecorded
+ );
assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled);
assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled);
+
+ let payment_payload = payment_recorded_payload(
+ &request_payload,
+ request_event.id.as_str(),
+ decision_event.id.as_str(),
+ decision_event.id.as_str(),
+ );
+ let payment_parts = active_trade_payment_recorded_event_build(
+ request_event.id.as_str(),
+ decision_event.id.as_str(),
+ &payment_payload,
+ )
+ .expect("build payment recorded event");
+ let payment_event =
+ event_from_parts("buyer-order-payment-event", buyer_pubkey, payment_parts);
+ events
+ .append_record(&signed_order_event_record(
+ "app:signed_event:order-payment:buyer",
+ &payment_event,
+ listing_addr.as_str(),
+ SourceRuntime::App,
+ Some("acct_buyer"),
+ ))
+ .expect("append payment recorded event");
+
+ let payment_report = app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import payment recorded event");
+ let buyer_orders = app_store
+ .load_buyer_orders(&buyer_context)
+ .expect("load buyer orders after payment");
+ let buyer_detail = app_store
+ .load_buyer_order_detail(&buyer_context, order_id)
+ .expect("load buyer order detail after payment")
+ .expect("buyer order detail after payment");
+
+ assert_eq!(payment_report.imported_records, 1);
+ assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Scheduled);
+ assert_eq!(
+ buyer_orders.rows[0].workflow.payment,
+ TradePaymentDisplayStatus::Recorded
+ );
+ assert_eq!(
+ buyer_orders.rows[0].workflow.provenance.primary_source,
+ TradeWorkflowSource::LocalEvents
+ );
+ assert_eq!(
+ buyer_orders.rows[0]
+ .workflow
+ .provenance
+ .last_event_id
+ .as_deref(),
+ Some(decision_event.id.as_str())
+ );
+ assert_eq!(buyer_detail.payment, TradePaymentDisplayStatus::Recorded);
+ assert_eq!(buyer_detail.workflow, buyer_orders.rows[0].workflow);
}
#[test]
@@ -6367,16 +6526,16 @@ mod tests {
)
.expect("buyer cart should save");
app_store
- .save_buyer_checkout_draft(
+ .save_buyer_order_review_draft(
&buyer_context,
- &radroots_app_view::BuyerCheckoutDraft {
+ &radroots_app_view::BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.test".to_owned(),
phone: String::new(),
order_note: String::new(),
},
)
- .expect("checkout draft should save");
+ .expect("order review draft should save");
let order_id = app_store
.place_buyer_order(&buyer_context)
.expect("buyer order should place");
diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs
@@ -14,8 +14,8 @@ use radroots_app_sync::{
};
use radroots_app_view::{
AccountSurfaceActivationProjection, AppActivityContext, AppActivityEvent, AppActivityKind,
- BuyerCartProjection, BuyerCheckoutDraft, BuyerCheckoutProjection, BuyerContext,
- BuyerListingsProjection, BuyerOrderDetailProjection, BuyerOrdersProjection,
+ BuyerCartProjection, BuyerContext, BuyerListingsProjection, BuyerOrderDetailProjection,
+ BuyerOrderReviewDraft, BuyerOrderReviewProjection, BuyerOrdersProjection,
BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmRulesProjection,
FarmSetupProjection, FarmSummary, FulfillmentWindowId, OrderDetailProjection, OrderId,
OrderRecoveryProjection, OrdersListProjection, OrdersScreenQueryState, PackDayOutputSource,
@@ -421,20 +421,20 @@ impl AppSqliteStore {
self.buyer_repository().clear_buyer_cart(context)
}
- pub fn load_buyer_checkout(
+ pub fn load_buyer_order_review(
&self,
context: &BuyerContext,
- ) -> Result<BuyerCheckoutProjection, AppSqliteError> {
- self.buyer_repository().load_buyer_checkout(context)
+ ) -> Result<BuyerOrderReviewProjection, AppSqliteError> {
+ self.buyer_repository().load_buyer_order_review(context)
}
- pub fn save_buyer_checkout_draft(
+ pub fn save_buyer_order_review_draft(
&self,
context: &BuyerContext,
- draft: &BuyerCheckoutDraft,
+ draft: &BuyerOrderReviewDraft,
) -> Result<(), AppSqliteError> {
self.buyer_repository()
- .save_buyer_checkout_draft(context, draft)
+ .save_buyer_order_review_draft(context, draft)
}
pub fn place_buyer_order(&self, context: &BuyerContext) -> Result<OrderId, AppSqliteError> {
diff --git a/crates/store/src/migrations.rs b/crates/store/src/migrations.rs
@@ -92,6 +92,10 @@ const MIGRATIONS: &[Migration] = &[
version: 22,
sql: include_str!("../migrations/0022_order_workflow_revision.sql"),
},
+ Migration {
+ version: 23,
+ sql: include_str!("../migrations/0023_order_workflow_display_projection.sql"),
+ },
];
pub fn latest_schema_version() -> u32 {
diff --git a/crates/store/src/repo/buyer.rs b/crates/store/src/repo/buyer.rs
@@ -2,14 +2,16 @@ use std::collections::{BTreeMap, BTreeSet};
use radroots_app_view::{
BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection,
- BuyerCheckoutDisabledReason, BuyerCheckoutDraft, BuyerCheckoutProjection,
- BuyerCheckoutSummaryProjection, BuyerContext, BuyerListingRow, BuyerListingsProjection,
- BuyerOrderDetailProjection, BuyerOrderStatus, BuyerOrdersListRow, BuyerOrdersProjection,
+ BuyerContext, BuyerListingRow, BuyerListingsProjection, BuyerOrderDetailProjection,
+ BuyerOrderReviewDisabledReason, BuyerOrderReviewDraft, BuyerOrderReviewProjection,
+ BuyerOrderReviewSummaryProjection, BuyerOrderStatus, BuyerOrdersListRow, BuyerOrdersProjection,
BuyerProductDetailProjection, FarmId, FarmOrderMethod, FulfillmentWindowId, OrderDetailItemRow,
OrderId, OrderStatus, ProductAvailabilityState, ProductAvailabilitySummary, ProductId,
ProductPricePresentation, ProductStatus, ProductStockState, ProductStockSummary,
- RepeatDemandEligibility, RepeatDemandHandoffProjection, TradePaymentDisplayStatus,
- TradeWorkflowProjection,
+ RepeatDemandEligibility, RepeatDemandHandoffProjection, TradeAgreementStatus,
+ TradeEconomicsProjection, TradeFulfillmentStatus, TradeInventoryStatus,
+ TradePaymentDisplayStatus, TradeProvenanceProjection, TradeRevisionStatus,
+ TradeWorkflowProjection, TradeWorkflowSource,
};
use rusqlite::{Connection, OptionalExtension, params};
use serde_json::Value;
@@ -270,24 +272,28 @@ impl<'a> AppBuyerRepository<'a> {
Ok(())
}
- pub fn load_buyer_checkout(
+ pub fn load_buyer_order_review(
&self,
context: &BuyerContext,
- ) -> Result<BuyerCheckoutProjection, AppSqliteError> {
+ ) -> Result<BuyerOrderReviewProjection, AppSqliteError> {
let context_key = context.storage_key();
let header = self.load_cart_header(&context_key)?;
let cart =
self.build_cart_projection(header.clone(), self.load_cart_line_records(&context_key)?)?;
let draft = header
- .map(BuyerCartHeader::into_checkout_draft)
+ .map(BuyerCartHeader::into_order_review_draft)
.unwrap_or_default();
let fulfillment_summary = shared_fulfillment_summary(&cart.lines);
- let place_order_disabled_reason =
- buyer_checkout_disabled_reason(context, &cart, fulfillment_summary.as_ref(), &draft);
+ let place_order_disabled_reason = buyer_order_review_disabled_reason(
+ context,
+ &cart,
+ fulfillment_summary.as_ref(),
+ &draft,
+ );
- Ok(BuyerCheckoutProjection {
+ Ok(BuyerOrderReviewProjection {
draft: draft.clone(),
- summary: BuyerCheckoutSummaryProjection {
+ summary: BuyerOrderReviewSummaryProjection {
farm_display_name: cart.farm_display_name.clone(),
fulfillment_summary: fulfillment_summary.clone(),
line_count: cart.lines.len() as u32,
@@ -299,10 +305,10 @@ impl<'a> AppBuyerRepository<'a> {
})
}
- pub fn save_buyer_checkout_draft(
+ pub fn save_buyer_order_review_draft(
&self,
context: &BuyerContext,
- draft: &BuyerCheckoutDraft,
+ draft: &BuyerOrderReviewDraft,
) -> Result<(), AppSqliteError> {
let context_key = context.storage_key();
@@ -317,7 +323,7 @@ impl<'a> AppBuyerRepository<'a> {
params![context_key.as_str()],
)
.map_err(|source| AppSqliteError::Query {
- operation: "ensure buyer checkout header",
+ operation: "ensure buyer order review header",
source,
})?;
self.connection
@@ -339,7 +345,7 @@ impl<'a> AppBuyerRepository<'a> {
],
)
.map_err(|source| AppSqliteError::Query {
- operation: "save buyer checkout draft",
+ operation: "save buyer order review draft",
source,
})?;
@@ -355,11 +361,11 @@ impl<'a> AppBuyerRepository<'a> {
})?;
let line_records = self.load_cart_line_records(&context_key)?;
let cart = self.build_cart_projection(Some(header.clone()), line_records.clone())?;
- let checkout = self.load_buyer_checkout(context)?;
+ let order_review = self.load_buyer_order_review(context)?;
- if let Some(disabled_reason) = checkout.place_order_disabled_reason {
+ if let Some(disabled_reason) = order_review.place_order_disabled_reason {
return Err(AppSqliteError::InvalidProjection {
- reason: buyer_checkout_disabled_error(disabled_reason),
+ reason: buyer_order_review_disabled_error(disabled_reason),
});
}
@@ -373,7 +379,7 @@ impl<'a> AppBuyerRepository<'a> {
self.connection
.execute_batch("begin immediate transaction")
.map_err(|source| AppSqliteError::Query {
- operation: "begin buyer checkout write",
+ operation: "begin buyer order review write",
source,
})?;
@@ -410,11 +416,11 @@ impl<'a> AppBuyerRepository<'a> {
farm_id.to_string(),
fulfillment_window_id.map(|id| id.to_string()),
order_number,
- checkout.draft.name.trim(),
+ order_review.draft.name.trim(),
context_key.as_str(),
- checkout.draft.email.trim(),
- checkout.draft.phone.trim(),
- checkout.draft.order_note.trim(),
+ order_review.draft.email.trim(),
+ order_review.draft.phone.trim(),
+ order_review.draft.order_note.trim(),
],
)
.map_err(|source| AppSqliteError::Query {
@@ -473,7 +479,7 @@ impl<'a> AppBuyerRepository<'a> {
params![context_key.as_str()],
)
.map_err(|source| AppSqliteError::Query {
- operation: "clear buyer cart lines after checkout",
+ operation: "clear buyer cart lines after order review",
source,
})?;
self.connection
@@ -487,7 +493,7 @@ impl<'a> AppBuyerRepository<'a> {
params![context_key.as_str()],
)
.map_err(|source| AppSqliteError::Query {
- operation: "reset buyer cart header after checkout",
+ operation: "reset buyer cart header after order review",
source,
})?;
self.insert_pending_buyer_order_coordination(context_key.as_str(), order_id)?;
@@ -499,7 +505,7 @@ impl<'a> AppBuyerRepository<'a> {
Ok(order_id) => {
self.connection.execute_batch("commit").map_err(|source| {
AppSqliteError::Query {
- operation: "commit buyer checkout write",
+ operation: "commit buyer order review write",
source,
}
})?;
@@ -744,6 +750,12 @@ impl<'a> AppBuyerRepository<'a> {
o.order_number,
o.status,
o.workflow_revision,
+ o.workflow_agreement,
+ o.workflow_fulfillment,
+ o.workflow_inventory,
+ o.workflow_payment,
+ o.workflow_provenance_source,
+ o.workflow_provenance_last_event_id,
f.display_name,
fw.label,
fw.starts_at,
@@ -768,8 +780,14 @@ impl<'a> AppBuyerRepository<'a> {
row.get::<_, String>(4)?,
row.get::<_, String>(5)?,
row.get::<_, Option<String>>(6)?,
- row.get::<_, Option<String>>(7)?,
- row.get::<_, Option<String>>(8)?,
+ row.get::<_, String>(7)?,
+ row.get::<_, String>(8)?,
+ row.get::<_, String>(9)?,
+ row.get::<_, Option<String>>(10)?,
+ row.get::<_, String>(11)?,
+ row.get::<_, Option<String>>(12)?,
+ row.get::<_, Option<String>>(13)?,
+ row.get::<_, Option<String>>(14)?,
))
})
.map_err(|source| AppSqliteError::Query {
@@ -785,6 +803,12 @@ impl<'a> AppBuyerRepository<'a> {
order_number,
status,
workflow_revision,
+ workflow_agreement,
+ workflow_fulfillment,
+ workflow_inventory,
+ workflow_payment,
+ workflow_provenance_source,
+ workflow_provenance_last_event_id,
farm_display_name,
fulfillment_label,
fulfillment_starts_at,
@@ -793,11 +817,24 @@ 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 order_id: OrderId = parse_typed_id("orders.id", order_id)?;
+ let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?;
let buyer_status = BuyerOrderStatus::from(parse_order_status("orders.status", status)?);
let revision =
parse_trade_revision_status("orders.workflow_revision", workflow_revision)?;
+ let items = self.load_order_detail_items(order_id.to_string())?;
+ let economics = order_detail_economics(&items)?;
+ let workflow = trade_workflow_projection_from_storage(
+ order_id,
+ revision,
+ economics,
+ workflow_agreement,
+ workflow_fulfillment,
+ workflow_inventory,
+ workflow_payment,
+ workflow_provenance_source,
+ workflow_provenance_last_event_id,
+ )?;
orders.push(BuyerOrdersListRow {
order_id,
@@ -816,8 +853,7 @@ impl<'a> AppBuyerRepository<'a> {
fulfillment_ends_at,
),
status: buyer_status,
- workflow: TradeWorkflowProjection::from_buyer_order_status(order_id, buyer_status)
- .with_revision(revision),
+ workflow,
});
}
@@ -842,6 +878,12 @@ impl<'a> AppBuyerRepository<'a> {
o.status,
o.buyer_order_note,
o.workflow_revision,
+ o.workflow_agreement,
+ o.workflow_fulfillment,
+ o.workflow_inventory,
+ o.workflow_payment,
+ o.workflow_provenance_source,
+ o.workflow_provenance_last_event_id,
f.display_name,
fw.label,
fw.starts_at,
@@ -862,8 +904,14 @@ impl<'a> AppBuyerRepository<'a> {
row.get::<_, String>(5)?,
row.get::<_, String>(6)?,
row.get::<_, Option<String>>(7)?,
- row.get::<_, Option<String>>(8)?,
- row.get::<_, Option<String>>(9)?,
+ row.get::<_, String>(8)?,
+ row.get::<_, String>(9)?,
+ row.get::<_, String>(10)?,
+ row.get::<_, Option<String>>(11)?,
+ row.get::<_, String>(12)?,
+ row.get::<_, Option<String>>(13)?,
+ row.get::<_, Option<String>>(14)?,
+ row.get::<_, Option<String>>(15)?,
))
},
)
@@ -882,6 +930,12 @@ impl<'a> AppBuyerRepository<'a> {
status,
order_note,
workflow_revision,
+ workflow_agreement,
+ workflow_fulfillment,
+ workflow_inventory,
+ workflow_payment,
+ workflow_provenance_source,
+ workflow_provenance_last_event_id,
farm_display_name,
fulfillment_label,
fulfillment_starts_at,
@@ -895,11 +949,18 @@ impl<'a> AppBuyerRepository<'a> {
parse_trade_revision_status("orders.workflow_revision", workflow_revision)?;
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_revision(revision)
- .with_economics_and_payment(economics.clone(), payment);
+ let workflow = trade_workflow_projection_from_storage(
+ order_id,
+ revision,
+ economics.clone(),
+ workflow_agreement,
+ workflow_fulfillment,
+ workflow_inventory,
+ workflow_payment,
+ workflow_provenance_source,
+ workflow_provenance_last_event_id,
+ )?;
+ let payment = workflow.payment;
Ok(BuyerOrderDetailProjection {
order_id,
farm_id,
@@ -2064,8 +2125,8 @@ struct BuyerCartHeader {
}
impl BuyerCartHeader {
- fn into_checkout_draft(self) -> BuyerCheckoutDraft {
- BuyerCheckoutDraft {
+ fn into_order_review_draft(self) -> BuyerOrderReviewDraft {
+ BuyerOrderReviewDraft {
name: self.buyer_name,
email: self.buyer_email,
phone: self.buyer_phone,
@@ -2474,6 +2535,104 @@ fn refresh_buyer_cart_summary(cart: &mut BuyerCartProjection) -> Result<(), AppS
Ok(())
}
+fn trade_workflow_projection_from_storage(
+ order_id: OrderId,
+ revision: TradeRevisionStatus,
+ economics: TradeEconomicsProjection,
+ agreement: String,
+ fulfillment: Option<String>,
+ inventory: String,
+ payment: String,
+ provenance_source: String,
+ provenance_last_event_id: Option<String>,
+) -> Result<TradeWorkflowProjection, AppSqliteError> {
+ Ok(TradeWorkflowProjection {
+ order_id,
+ agreement: parse_trade_agreement_status("orders.workflow_agreement", agreement)?,
+ revision,
+ fulfillment: fulfillment
+ .map(|value| parse_trade_fulfillment_status("orders.workflow_fulfillment", value))
+ .transpose()?,
+ economics,
+ inventory: parse_trade_inventory_status("orders.workflow_inventory", inventory)?,
+ payment: parse_trade_payment_display_status("orders.workflow_payment", payment)?,
+ provenance: TradeProvenanceProjection::from_primary_source(parse_trade_workflow_source(
+ "orders.workflow_provenance_source",
+ provenance_source,
+ )?)
+ .with_last_event_id(provenance_last_event_id),
+ })
+}
+
+fn parse_trade_agreement_status(
+ field: &'static str,
+ value: String,
+) -> Result<TradeAgreementStatus, AppSqliteError> {
+ match value.as_str() {
+ "ordered" => Ok(TradeAgreementStatus::Ordered),
+ "confirmed" => Ok(TradeAgreementStatus::Confirmed),
+ "declined" => Ok(TradeAgreementStatus::Declined),
+ "cancelled" => Ok(TradeAgreementStatus::Cancelled),
+ "completed" => Ok(TradeAgreementStatus::Completed),
+ "needs_review" => Ok(TradeAgreementStatus::NeedsReview),
+ _ => Err(AppSqliteError::DecodeEnum { field, value }),
+ }
+}
+
+fn parse_trade_fulfillment_status(
+ field: &'static str,
+ value: String,
+) -> Result<TradeFulfillmentStatus, AppSqliteError> {
+ match value.as_str() {
+ "confirmed" => Ok(TradeFulfillmentStatus::Confirmed),
+ "preparing" => Ok(TradeFulfillmentStatus::Preparing),
+ "ready_for_pickup" => Ok(TradeFulfillmentStatus::ReadyForPickup),
+ "out_for_delivery" => Ok(TradeFulfillmentStatus::OutForDelivery),
+ "delivered" => Ok(TradeFulfillmentStatus::Delivered),
+ "cancelled" => Ok(TradeFulfillmentStatus::Cancelled),
+ _ => Err(AppSqliteError::DecodeEnum { field, value }),
+ }
+}
+
+fn parse_trade_inventory_status(
+ field: &'static str,
+ value: String,
+) -> Result<TradeInventoryStatus, AppSqliteError> {
+ match value.as_str() {
+ "available" => Ok(TradeInventoryStatus::Available),
+ "reserved" => Ok(TradeInventoryStatus::Reserved),
+ "sold_out" => Ok(TradeInventoryStatus::SoldOut),
+ "needs_review" => Ok(TradeInventoryStatus::NeedsReview),
+ _ => Err(AppSqliteError::DecodeEnum { field, value }),
+ }
+}
+
+fn parse_trade_payment_display_status(
+ field: &'static str,
+ value: String,
+) -> Result<TradePaymentDisplayStatus, AppSqliteError> {
+ match value.as_str() {
+ "not_recorded" => Ok(TradePaymentDisplayStatus::NotRecorded),
+ "recorded" => Ok(TradePaymentDisplayStatus::Recorded),
+ "needs_review" => Ok(TradePaymentDisplayStatus::NeedsReview),
+ _ => Err(AppSqliteError::DecodeEnum { field, value }),
+ }
+}
+
+fn parse_trade_workflow_source(
+ field: &'static str,
+ value: String,
+) -> Result<TradeWorkflowSource, AppSqliteError> {
+ match value.as_str() {
+ "app" => Ok(TradeWorkflowSource::App),
+ "cli" => Ok(TradeWorkflowSource::Cli),
+ "relay" => Ok(TradeWorkflowSource::Relay),
+ "local_events" => Ok(TradeWorkflowSource::LocalEvents),
+ "unknown" => Ok(TradeWorkflowSource::Unknown),
+ _ => Err(AppSqliteError::DecodeEnum { field, value }),
+ }
+}
+
fn shared_fulfillment_summary(lines: &[BuyerCartLineProjection]) -> Option<String> {
let first = lines.first()?.fulfillment_summary.clone();
@@ -2483,40 +2642,40 @@ fn shared_fulfillment_summary(lines: &[BuyerCartLineProjection]) -> Option<Strin
.then_some(first)
}
-fn buyer_checkout_disabled_reason(
+fn buyer_order_review_disabled_reason(
context: &BuyerContext,
cart: &BuyerCartProjection,
fulfillment_summary: Option<&String>,
- draft: &BuyerCheckoutDraft,
-) -> Option<BuyerCheckoutDisabledReason> {
+ draft: &BuyerOrderReviewDraft,
+) -> Option<BuyerOrderReviewDisabledReason> {
if cart.lines.is_empty() {
- return Some(BuyerCheckoutDisabledReason::EmptyCart);
+ return Some(BuyerOrderReviewDisabledReason::EmptyCart);
}
if fulfillment_summary.is_none() {
- return Some(BuyerCheckoutDisabledReason::MissingFulfillment);
+ return Some(BuyerOrderReviewDisabledReason::MissingFulfillment);
}
if draft.name.trim().is_empty() {
- return Some(BuyerCheckoutDisabledReason::MissingName);
+ return Some(BuyerOrderReviewDisabledReason::MissingName);
}
if draft.email.trim().is_empty() {
- return Some(BuyerCheckoutDisabledReason::MissingEmail);
+ return Some(BuyerOrderReviewDisabledReason::MissingEmail);
}
if matches!(context, BuyerContext::Guest) {
- return Some(BuyerCheckoutDisabledReason::AccountRequired);
+ return Some(BuyerOrderReviewDisabledReason::AccountRequired);
}
None
}
-fn buyer_checkout_disabled_error(reason: BuyerCheckoutDisabledReason) -> &'static str {
+fn buyer_order_review_disabled_error(reason: BuyerOrderReviewDisabledReason) -> &'static str {
match reason {
- BuyerCheckoutDisabledReason::EmptyCart => "buyer checkout cart is empty",
- BuyerCheckoutDisabledReason::MissingFulfillment => {
- "buyer checkout fulfillment is unavailable"
+ BuyerOrderReviewDisabledReason::EmptyCart => "buyer order review cart is empty",
+ BuyerOrderReviewDisabledReason::MissingFulfillment => {
+ "buyer order review fulfillment is unavailable"
}
- BuyerCheckoutDisabledReason::MissingName => "buyer checkout buyer name is missing",
- BuyerCheckoutDisabledReason::MissingEmail => "buyer checkout buyer email is missing",
- BuyerCheckoutDisabledReason::AccountRequired => {
- "buyer checkout requires a selected account"
+ BuyerOrderReviewDisabledReason::MissingName => "buyer order review buyer name is missing",
+ BuyerOrderReviewDisabledReason::MissingEmail => "buyer order review buyer email is missing",
+ BuyerOrderReviewDisabledReason::AccountRequired => {
+ "buyer order review requires a selected account"
}
}
}
@@ -2538,7 +2697,7 @@ fn shared_fulfillment_window_id(
Ok(first_window_id)
} else {
Err(AppSqliteError::InvalidProjection {
- reason: "buyer cart must share one fulfillment window at checkout",
+ reason: "buyer cart must share one fulfillment window at order review",
})
}
}
@@ -2795,8 +2954,9 @@ mod tests {
use std::collections::BTreeSet;
use radroots_app_view::{
- BuyerCheckoutDisabledReason, BuyerContext, FarmId, FarmOrderMethod, FulfillmentWindowId,
- OrderId, PickupLocationId, ProductId, TradePaymentDisplayStatus, TradeRevisionStatus,
+ BuyerContext, BuyerOrderReviewDisabledReason, FarmId, FarmOrderMethod, FulfillmentWindowId,
+ OrderId, PickupLocationId, ProductId, TradeAgreementStatus, TradeFulfillmentStatus,
+ TradeInventoryStatus, TradePaymentDisplayStatus, TradeRevisionStatus, TradeWorkflowSource,
};
use rusqlite::{Connection, params};
use serde_json::json;
@@ -2942,7 +3102,7 @@ mod tests {
}
#[test]
- fn buyer_checkout_requires_account_before_order_write() {
+ fn buyer_order_review_requires_account_before_order_write() {
let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
let connection = store.connection();
let repository = AppBuyerRepository::new(connection);
@@ -3002,36 +3162,36 @@ mod tests {
)
.expect("buyer cart should save");
repository
- .save_buyer_checkout_draft(
+ .save_buyer_order_review_draft(
&context,
- &radroots_app_view::BuyerCheckoutDraft {
+ &radroots_app_view::BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: "555-0101".to_owned(),
order_note: "Leave by the cooler".to_owned(),
},
)
- .expect("buyer checkout draft should save");
+ .expect("buyer order review draft should save");
- let checkout = repository
- .load_buyer_checkout(&context)
- .expect("buyer checkout should load");
+ let order_review = repository
+ .load_buyer_order_review(&context)
+ .expect("buyer order review should load");
let error = repository
.place_buyer_order(&context)
- .expect_err("guest checkout should require an account");
- let cart_after_checkout = repository
+ .expect_err("guest order review should require an account");
+ let cart_after_order_review = repository
.load_buyer_cart(&context)
- .expect("buyer cart should remain after blocked checkout");
+ .expect("buyer cart should remain after blocked order review");
assert!(matches!(error, AppSqliteError::InvalidProjection { .. }));
- assert!(!checkout.can_place_order);
+ assert!(!order_review.can_place_order);
assert_eq!(
- checkout.place_order_disabled_reason,
- Some(BuyerCheckoutDisabledReason::AccountRequired)
+ order_review.place_order_disabled_reason,
+ Some(BuyerOrderReviewDisabledReason::AccountRequired)
);
- assert_eq!(checkout.summary.line_count, 1);
- assert_eq!(cart_after_checkout.lines.len(), 1);
- assert_eq!(cart_after_checkout.farm_id, Some(farm_id));
+ assert_eq!(order_review.summary.line_count, 1);
+ assert_eq!(cart_after_order_review.lines.len(), 1);
+ assert_eq!(cart_after_order_review.farm_id, Some(farm_id));
assert_eq!(row_count(connection, "orders"), 0);
assert_eq!(row_count(connection, "order_lines"), 0);
assert_eq!(row_count(connection, "buyer_order_coordination_records"), 0);
@@ -3132,19 +3292,19 @@ mod tests {
)
.expect("buyer cart should save");
repository
- .save_buyer_checkout_draft(
+ .save_buyer_order_review_draft(
&context,
- &radroots_app_view::BuyerCheckoutDraft {
+ &radroots_app_view::BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: String::new(),
order_note: String::new(),
},
)
- .expect("buyer checkout draft should save");
+ .expect("buyer order review draft should save");
let order_id = repository
.place_buyer_order(&context)
- .expect("buyer checkout should place order");
+ .expect("buyer order review should place order");
connection
.execute(
@@ -3262,19 +3422,19 @@ mod tests {
)
.expect("buyer cart should save");
repository
- .save_buyer_checkout_draft(
+ .save_buyer_order_review_draft(
&context,
- &radroots_app_view::BuyerCheckoutDraft {
+ &radroots_app_view::BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: String::new(),
order_note: String::new(),
},
)
- .expect("buyer checkout draft should save");
+ .expect("buyer order review draft should save");
let order_id = repository
.place_buyer_order(&context)
- .expect("buyer checkout should place order");
+ .expect("buyer order review should place order");
connection
.execute(
@@ -3419,6 +3579,84 @@ mod tests {
}
#[test]
+ fn buyer_order_projections_read_workflow_display_snapshot() {
+ let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
+ let connection = store.connection();
+ let repository = AppBuyerRepository::new(connection);
+ let context = BuyerContext::account("acct_buyer");
+ let farm_id = insert_farm(connection, "Willow Farm", "ready");
+ let order_id = OrderId::new();
+ let product_id = ProductId::new();
+
+ insert_order(
+ connection,
+ order_id,
+ farm_id,
+ "R-100",
+ "needs_action",
+ Some("account:acct_buyer"),
+ "buyer@example.com",
+ "",
+ "",
+ );
+ insert_order_line(
+ connection,
+ order_id,
+ product_id,
+ "Salad mix",
+ 2,
+ "bag",
+ Some(650),
+ "USD",
+ );
+ set_order_workflow_revision(
+ connection,
+ order_id,
+ TradeRevisionStatus::KeptAsPlaced.storage_key(),
+ );
+ set_order_workflow_display_projection(
+ connection,
+ order_id,
+ "confirmed",
+ Some("ready_for_pickup"),
+ "reserved",
+ "recorded",
+ "local_events",
+ Some("payment-event-1"),
+ );
+
+ let list = repository
+ .load_buyer_orders(&context)
+ .expect("buyer order list should load");
+ let detail = repository
+ .load_buyer_order_detail(&context, order_id)
+ .expect("buyer order detail should load")
+ .expect("buyer order detail should exist");
+ let row = &list.rows[0];
+
+ assert_eq!(list.rows.len(), 1);
+ assert_eq!(row.workflow.agreement, TradeAgreementStatus::Confirmed);
+ assert_eq!(
+ row.workflow.fulfillment,
+ Some(TradeFulfillmentStatus::ReadyForPickup)
+ );
+ assert_eq!(row.workflow.inventory, TradeInventoryStatus::Reserved);
+ assert_eq!(row.workflow.payment, TradePaymentDisplayStatus::Recorded);
+ assert_eq!(
+ row.workflow.provenance.primary_source,
+ TradeWorkflowSource::LocalEvents
+ );
+ assert_eq!(
+ row.workflow.provenance.last_event_id.as_deref(),
+ Some("payment-event-1")
+ );
+ assert_eq!(row.workflow.economics.total_minor_units, Some(1300));
+ assert_eq!(row.workflow.economics.currency_code.as_deref(), Some("USD"));
+ assert_eq!(detail.workflow, row.workflow);
+ assert_eq!(detail.payment, TradePaymentDisplayStatus::Recorded);
+ }
+
+ #[test]
fn buyer_cart_rejects_cross_farm_lines() {
let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
let farm_id = FarmId::new();
@@ -3684,6 +3922,76 @@ mod tests {
.expect("order workflow revision update should succeed");
}
+ fn set_order_workflow_display_projection(
+ connection: &Connection,
+ order_id: OrderId,
+ agreement: &str,
+ fulfillment: Option<&str>,
+ inventory: &str,
+ payment: &str,
+ provenance_source: &str,
+ provenance_last_event_id: Option<&str>,
+ ) {
+ connection
+ .execute(
+ "update orders
+ set workflow_agreement = ?1,
+ workflow_fulfillment = ?2,
+ workflow_inventory = ?3,
+ workflow_payment = ?4,
+ workflow_provenance_source = ?5,
+ workflow_provenance_last_event_id = ?6
+ where id = ?7",
+ params![
+ agreement,
+ fulfillment,
+ inventory,
+ payment,
+ provenance_source,
+ provenance_last_event_id,
+ order_id.to_string(),
+ ],
+ )
+ .expect("order workflow display projection update should succeed");
+ }
+
+ fn insert_order_line(
+ connection: &Connection,
+ order_id: OrderId,
+ product_id: ProductId,
+ title: &str,
+ quantity: i64,
+ unit_label: &str,
+ unit_price_minor_units: Option<u32>,
+ price_currency: &str,
+ ) {
+ connection
+ .execute(
+ "insert into order_lines (
+ id,
+ order_id,
+ title,
+ quantity_value,
+ quantity_unit_label,
+ quantity_display,
+ unit_price_minor_units,
+ price_currency,
+ sort_index
+ ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0)",
+ params![
+ format!("{order_id}:{product_id}"),
+ order_id.to_string(),
+ title,
+ quantity,
+ unit_label,
+ format!("{quantity} {unit_label}"),
+ unit_price_minor_units,
+ price_currency,
+ ],
+ )
+ .expect("order line insert should succeed");
+ }
+
fn corrupt_order_workflow_revision(
connection: &Connection,
order_id: OrderId,
diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs
@@ -1017,7 +1017,7 @@ impl BuyerCartProjection {
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
-pub struct BuyerCheckoutDraft {
+pub struct BuyerOrderReviewDraft {
pub name: String,
pub email: String,
pub phone: String,
@@ -1025,7 +1025,7 @@ pub struct BuyerCheckoutDraft {
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
-pub struct BuyerCheckoutSummaryProjection {
+pub struct BuyerOrderReviewSummaryProjection {
pub farm_display_name: Option<String>,
pub fulfillment_summary: Option<String>,
pub line_count: u32,
@@ -1035,7 +1035,7 @@ pub struct BuyerCheckoutSummaryProjection {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
-pub enum BuyerCheckoutDisabledReason {
+pub enum BuyerOrderReviewDisabledReason {
EmptyCart,
MissingFulfillment,
MissingName,
@@ -1043,7 +1043,7 @@ pub enum BuyerCheckoutDisabledReason {
AccountRequired,
}
-impl BuyerCheckoutDisabledReason {
+impl BuyerOrderReviewDisabledReason {
pub const fn storage_key(self) -> &'static str {
match self {
Self::EmptyCart => "empty_cart",
@@ -1056,11 +1056,11 @@ impl BuyerCheckoutDisabledReason {
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
-pub struct BuyerCheckoutProjection {
- pub draft: BuyerCheckoutDraft,
- pub summary: BuyerCheckoutSummaryProjection,
+pub struct BuyerOrderReviewProjection {
+ pub draft: BuyerOrderReviewDraft,
+ pub summary: BuyerOrderReviewSummaryProjection,
pub can_place_order: bool,
- pub place_order_disabled_reason: Option<BuyerCheckoutDisabledReason>,
+ pub place_order_disabled_reason: Option<BuyerOrderReviewDisabledReason>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
@@ -2189,19 +2189,20 @@ mod tests {
AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface,
ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind,
AppIdentityProjection, AppStartupGate, BlackoutPeriodId, BuyerCartLineProjection,
- BuyerCartProjection, BuyerCheckoutDisabledReason, BuyerCheckoutDraft,
- BuyerCheckoutProjection, BuyerCheckoutSummaryProjection, BuyerContext, BuyerListingRow,
- BuyerListingsProjection, BuyerOrderDetailProjection, BuyerOrderStatus, BuyerOrdersListRow,
- BuyerOrdersProjection, FarmId, FarmOrderMethod, FarmReadinessBlocker, FarmRulesProjection,
- FarmRulesReadiness, FarmSetupBlocker, FarmSetupDraft, 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,
+ BuyerCartProjection, BuyerContext, BuyerListingRow, BuyerListingsProjection,
+ BuyerOrderDetailProjection, BuyerOrderReviewDisabledReason, BuyerOrderReviewDraft,
+ BuyerOrderReviewProjection, BuyerOrderReviewSummaryProjection, BuyerOrderStatus,
+ BuyerOrdersListRow, BuyerOrdersProjection, FarmId, FarmOrderMethod, FarmReadinessBlocker,
+ FarmRulesProjection, FarmRulesReadiness, FarmSetupBlocker, FarmSetupDraft,
+ 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,
PackDayHostHandoffStatus, PackDayOutputCustomerOrder, PackDayOutputOrderState,
PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity,
PackDayOutputSource, PackDayOutputWindow, PackDayPackListRow, PackDayPrintFailureKind,
@@ -2677,25 +2678,25 @@ mod tests {
}
#[test]
- fn buyer_checkout_disabled_reason_storage_keys_are_stable() {
+ fn buyer_order_review_disabled_reason_storage_keys_are_stable() {
assert_eq!(
- BuyerCheckoutDisabledReason::EmptyCart.storage_key(),
+ BuyerOrderReviewDisabledReason::EmptyCart.storage_key(),
"empty_cart"
);
assert_eq!(
- BuyerCheckoutDisabledReason::MissingFulfillment.storage_key(),
+ BuyerOrderReviewDisabledReason::MissingFulfillment.storage_key(),
"missing_fulfillment"
);
assert_eq!(
- BuyerCheckoutDisabledReason::MissingName.storage_key(),
+ BuyerOrderReviewDisabledReason::MissingName.storage_key(),
"missing_name"
);
assert_eq!(
- BuyerCheckoutDisabledReason::MissingEmail.storage_key(),
+ BuyerOrderReviewDisabledReason::MissingEmail.storage_key(),
"missing_email"
);
assert_eq!(
- BuyerCheckoutDisabledReason::AccountRequired.storage_key(),
+ BuyerOrderReviewDisabledReason::AccountRequired.storage_key(),
"account_required"
);
}
@@ -3615,14 +3616,14 @@ mod tests {
currency_code: Some("USD".to_owned()),
replace_confirmation: None,
};
- let checkout = BuyerCheckoutProjection {
- draft: BuyerCheckoutDraft {
+ let order_review = BuyerOrderReviewProjection {
+ draft: BuyerOrderReviewDraft {
name: "Casey Buyer".to_owned(),
email: "casey@example.com".to_owned(),
phone: String::new(),
order_note: "Leave by the cooler".to_owned(),
},
- summary: BuyerCheckoutSummaryProjection {
+ summary: BuyerOrderReviewSummaryProjection {
farm_display_name: Some("Cedar Grove Farm".to_owned()),
fulfillment_summary: Some("Thursday pickup".to_owned()),
line_count: 1,
@@ -3677,7 +3678,7 @@ mod tests {
assert!(!listings.is_empty());
assert!(!cart.is_empty());
- assert!(checkout.can_place_order);
+ assert!(order_review.can_place_order);
assert!(!orders.is_empty());
assert_eq!(listing.fulfillment_methods.len(), 1);
assert_eq!(order_detail.status, BuyerOrderStatus::Scheduled);
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -143,7 +143,7 @@
"personal.order_summary.title": "Order summary",
"personal.fulfillment.title": "Fulfillment",
"personal.cart.remove_line.action": "Remove",
- "personal.cart.continue_checkout.action": "Review order",
+ "personal.cart.review_order.action": "Review order",
"personal.cart.line.quantity.label": "Quantity",
"personal.cart.line.unit_price.label": "Unit price",
"personal.cart.line.total.label": "Line total",
@@ -157,15 +157,15 @@
"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": "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": "Review the details before placing the order.",
- "personal.checkout.place_order.action": "Place order",
+ "personal.order_review.title": "Order review",
+ "personal.order_review.back_action": "Back to cart",
+ "personal.order_review.contact.title": "Contact",
+ "personal.order_review.field.name": "Name",
+ "personal.order_review.field.email": "Email",
+ "personal.order_review.field.phone": "Phone",
+ "personal.order_review.field.order_note": "Order note",
+ "personal.order_review.local_only.body": "Review the details before placing the order.",
+ "personal.order_review.place_order.action": "Place order",
"orders.title": "Orders",
"orders.filters.title": "View",
"orders.summary.total": "Total orders",