app

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

commit 5a8586cb55addfbac9c9acc24bc92062d979abd2
parent 09db261376e23fe3b4ca0a3594f772ca2a982776
Author: triesap <tyson@radroots.org>
Date:   Tue,  2 Jun 2026 23:42:22 -0700

app: show order economics payment status

- Carry order line prices, totals, and neutral payment status through buyer and seller detail projections.

- Render localized total/payment rows and compact item price/line total displays.

- Replace app local order work no_payment payload with payment_display state.

Diffstat:
Mcrates/desktop/src/runtime.rs | 11+++++------
Mcrates/desktop/src/window.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/i18n/src/keys.rs | 4++++
Mcrates/state/src/lib.rs | 22++++++++++++++++++----
Mcrates/store/src/repo/buyer.rs | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mcrates/store/src/repo/mod.rs | 1+
Acrates/store/src/repo/order_detail.rs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/store/src/repo/orders.rs | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mcrates/view/src/lib.rs | 40++++++++++++++++++++++++++++++++++++----
Mi18n/locales/en/messages.json | 4++++
10 files changed, 366 insertions(+), 47 deletions(-)

diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -6669,10 +6669,9 @@ fn buyer_order_request_local_work_payload( "order_updated_at": order.updated_at, "created_at_ms": timestamp, }, - "no_payment": { - "payment_required": false, - "settlement_deferred": true, - "payment_state": "not_applicable", + "payment_display": { + "state": "not_recorded", + "allows_payment_action": false, }, "document": { "version": 1, @@ -13537,8 +13536,8 @@ mod tests { .as_ref() .expect("order local work payload"); assert_eq!(payload["support_status"]["state"], "supported"); - assert_eq!(payload["no_payment"]["payment_required"], false); - assert_eq!(payload["no_payment"]["settlement_deferred"], true); + assert_eq!(payload["payment_display"]["state"], "not_recorded"); + assert_eq!(payload["payment_display"]["allows_payment_action"], false); assert_eq!(payload["currentness"]["current"], true); assert_eq!(payload["document"]["kind"], "order_draft_v1"); assert_eq!( diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -65,7 +65,7 @@ use radroots_app_view::{ RecoveryKind, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, - TodaySetupTaskKind, + TodaySetupTaskKind, TradeEconomicsProjection, TradePaymentDisplayStatus, }; use radroots_nostr::prelude::RadrootsNostrClient; use std::{ @@ -4153,6 +4153,14 @@ impl HomeView { app_shared_text(AppTextKey::OrdersDetailPickupLabel), order_optional_text(detail.pickup_location_label.as_deref()), ), + LabelValueRow::new( + app_shared_text(AppTextKey::OrdersDetailTotalLabel), + trade_economics_total_text(&detail.economics), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::OrdersDetailPaymentLabel), + app_shared_text(trade_payment_display_status_key(detail.payment)), + ), ])) .child(app_form_section( app_shared_text(AppTextKey::OrdersDetailItemsTitle), @@ -8581,6 +8589,22 @@ fn buyer_money_text(amount_minor_units: u32, currency_code: &str) -> String { } } +fn trade_economics_total_text(economics: &TradeEconomicsProjection) -> String { + economics + .total_minor_units + .zip(economics.currency_code.as_deref()) + .map(|(amount, currency_code)| buyer_money_text(amount, currency_code)) + .unwrap_or_else(|| app_shared_text(AppTextKey::ValueNone).to_string()) +} + +fn trade_payment_display_status_key(status: TradePaymentDisplayStatus) -> AppTextKey { + match status { + TradePaymentDisplayStatus::NotRecorded => AppTextKey::TradeWorkflowPaymentNotRecorded, + TradePaymentDisplayStatus::Recorded => AppTextKey::TradeWorkflowPaymentRecorded, + TradePaymentDisplayStatus::NeedsReview => AppTextKey::TradeWorkflowPaymentNeedsReview, + } +} + fn buyer_orders_list_card( rows: &[BuyerOrdersListRow], selected_order_id: Option<OrderId>, @@ -8716,6 +8740,14 @@ fn buyer_order_detail_card( detail.fulfillment_summary.clone(), ), LabelValueRow::new( + app_shared_text(AppTextKey::PersonalOrdersDetailTotalLabel), + trade_economics_total_text(&detail.economics), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::PersonalOrdersDetailPaymentLabel), + app_shared_text(trade_payment_display_status_key(detail.payment)), + ), + LabelValueRow::new( app_shared_text(AppTextKey::PersonalOrdersDetailNoteLabel), order_optional_text(detail.order_note.as_deref()), ), @@ -12688,6 +12720,12 @@ fn home_list_card( } fn order_detail_item_row(item: &OrderDetailItemRow) -> AnyElement { + let unit_price = item.unit_price.as_ref().map(buyer_listing_price_text); + let line_total = item.unit_price.as_ref().and_then(|unit_price| { + item.line_total_minor_units + .map(|amount| buyer_money_text(amount, unit_price.currency_code.as_str())) + }); + div() .w_full() .min_w_0() @@ -12699,17 +12737,47 @@ fn order_detail_item_row(item: &OrderDetailItemRow) -> AnyElement { div() .flex_1() .min_w_0() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::MEDIUM) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(item.title.clone()), + .flex() + .flex_col() + .gap(px(2.0)) + .child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(item.title.clone()), + ) + .when_some(unit_price, |this, unit_price| { + this.child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(unit_price), + ) + }), ) .child( div() - .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(item.quantity_display.clone()), + .flex() + .flex_col() + .items_end() + .gap(px(2.0)) + .child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(item.quantity_display.clone()), + ) + .when_some(line_total, |this, line_total| { + this.child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(line_total), + ) + }), ) .into_any_element() } @@ -13075,6 +13143,7 @@ mod tests { ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, ReminderKind, ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + TradeEconomicsProjection, TradePaymentDisplayStatus, }; use radroots_identity::RadrootsIdentity; use std::{ @@ -13550,6 +13619,8 @@ mod tests { fulfillment_summary: String::new(), status: BuyerOrderStatus::Placed, items: Vec::new(), + economics: TradeEconomicsProjection::default(), + payment: TradePaymentDisplayStatus::NotRecorded, order_note: None, repeat_demand: Some(RepeatDemandHandoffProjection { order_id, @@ -13675,6 +13746,8 @@ mod tests { fulfillment_window_label: None, pickup_location_label: None, items: Vec::new(), + economics: TradeEconomicsProjection::default(), + payment: TradePaymentDisplayStatus::NotRecorded, primary_action: Some(OrderPrimaryAction::MarkPacked), recoveries: Vec::new(), }); diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -136,6 +136,8 @@ define_app_text_keys! { PersonalOrdersDetailFarmLabel => "personal.orders.detail.farm.label", PersonalOrdersDetailStatusLabel => "personal.orders.detail.status.label", PersonalOrdersDetailFulfillmentLabel => "personal.orders.detail.fulfillment.label", + PersonalOrdersDetailTotalLabel => "personal.orders.detail.total.label", + PersonalOrdersDetailPaymentLabel => "personal.orders.detail.payment.label", PersonalOrdersDetailNoteLabel => "personal.orders.detail.note.label", PersonalOrdersDetailItemsTitle => "personal.orders.detail.items.title", PersonalOrdersRepeatDemandTitle => "personal.orders.repeat_demand.title", @@ -210,6 +212,8 @@ define_app_text_keys! { OrdersDetailStatusLabel => "orders.detail.status.label", OrdersDetailWindowLabel => "orders.detail.window.label", OrdersDetailPickupLabel => "orders.detail.pickup.label", + OrdersDetailTotalLabel => "orders.detail.total.label", + OrdersDetailPaymentLabel => "orders.detail.payment.label", OrdersRecoverySectionTitle => "orders.recovery.section.title", OrdersRecoveryMissedPickupTitle => "orders.recovery.missed_pickup.title", OrdersRecoveryMissedPickupBody => "orders.recovery.missed_pickup.body", diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs @@ -2270,10 +2270,11 @@ mod tests { PackDayPrintLabelStock, PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PackDayScreenQueryState, PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductId, - ProductPublishBlocker, ProductsFilter, ProductsListProjection, ProductsSort, - ReminderDeliveryState, ReminderFeedProjection, ReminderKind, ReminderLogEntryProjection, - ReminderLogProjection, SelectedAccountProjection, SelectedSurfaceProjection, - SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + ProductPricePresentation, ProductPublishBlocker, ProductsFilter, ProductsListProjection, + ProductsSort, ReminderDeliveryState, ReminderFeedProjection, ReminderKind, + ReminderLogEntryProjection, ReminderLogProjection, SelectedAccountProjection, + SelectedSurfaceProjection, SettingsSection, ShellSection, TodayAgendaProjection, + TodaySetupTask, TodaySetupTaskKind, TradeEconomicsProjection, TradePaymentDisplayStatus, }; struct FailingRepository; @@ -2530,7 +2531,20 @@ mod tests { items: vec![OrderDetailItemRow { title: "Salad mix".to_owned(), quantity_display: "2 bags".to_owned(), + unit_price: Some(ProductPricePresentation { + amount_minor_units: 650, + currency_code: "USD".to_owned(), + unit_label: "bag".to_owned(), + }), + line_total_minor_units: Some(1300), }], + economics: TradeEconomicsProjection { + subtotal_minor_units: Some(1300), + total_minor_units: Some(1300), + currency_code: Some("USD".to_owned()), + ..TradeEconomicsProjection::default() + }, + payment: TradePaymentDisplayStatus::NotRecorded, primary_action: Some(OrderPrimaryAction::Review), recoveries: Vec::new(), }; diff --git a/crates/store/src/repo/buyer.rs b/crates/store/src/repo/buyer.rs @@ -8,11 +8,12 @@ use radroots_app_view::{ BuyerProductDetailProjection, FarmId, FarmOrderMethod, FulfillmentWindowId, OrderDetailItemRow, OrderId, OrderStatus, ProductAvailabilityState, ProductAvailabilitySummary, ProductId, ProductPricePresentation, ProductStatus, ProductStockState, ProductStockSummary, - RepeatDemandEligibility, RepeatDemandHandoffProjection, + RepeatDemandEligibility, RepeatDemandHandoffProjection, TradePaymentDisplayStatus, }; use rusqlite::{Connection, OptionalExtension, params}; use serde_json::Value; +use super::order_detail::{order_detail_economics, order_detail_item_row}; use crate::AppSqliteError; const BUYER_LOW_STOCK_THRESHOLD: u32 = 3; @@ -869,8 +870,10 @@ impl<'a> AppBuyerRepository<'a> { fulfillment_starts_at, fulfillment_ends_at, )| { - 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 items = self.load_order_detail_items(order_id.to_string())?; + let economics = order_detail_economics(&items)?; Ok(BuyerOrderDetailProjection { order_id, farm_id, @@ -885,7 +888,9 @@ impl<'a> AppBuyerRepository<'a> { "orders.status", status, )?), - items: self.load_order_detail_items(order_id.to_string())?, + items, + economics, + payment: TradePaymentDisplayStatus::NotRecorded, order_note: empty_string_to_none(order_note), repeat_demand: self.build_repeat_demand_handoff( order_id, @@ -1659,7 +1664,13 @@ impl<'a> AppBuyerRepository<'a> { let mut statement = self .connection .prepare( - "select title, quantity_display + "select + title, + quantity_display, + quantity_value, + quantity_unit_label, + unit_price_minor_units, + price_currency from order_lines where order_id = ?1 order by sort_index asc, id asc", @@ -1670,21 +1681,44 @@ impl<'a> AppBuyerRepository<'a> { })?; let rows = statement .query_map(params![order_id], |row| { - Ok(OrderDetailItemRow { - title: row.get(0)?, - quantity_display: row.get(1)?, - }) + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, i64>(2)?, + row.get::<_, String>(3)?, + row.get::<_, Option<u32>>(4)?, + row.get::<_, Option<String>>(5)?, + )) }) .map_err(|source| AppSqliteError::Query { operation: "query buyer order detail items", source, })?; + let mut items = Vec::new(); - rows.collect::<Result<Vec<_>, _>>() - .map_err(|source| AppSqliteError::Query { + for row in rows { + let ( + title, + quantity_display, + quantity_value, + quantity_unit_label, + unit_price_minor_units, + price_currency, + ) = row.map_err(|source| AppSqliteError::Query { operation: "read buyer order detail items", source, - }) + })?; + items.push(order_detail_item_row( + title, + quantity_display, + quantity_value, + quantity_unit_label, + unit_price_minor_units, + price_currency, + )?); + } + + Ok(items) } fn load_buyer_order_local_event_lines( @@ -2738,7 +2772,7 @@ mod tests { use radroots_app_view::{ BuyerCheckoutDisabledReason, BuyerContext, FarmId, FarmOrderMethod, FulfillmentWindowId, - OrderId, PickupLocationId, ProductId, + OrderId, PickupLocationId, ProductId, TradePaymentDisplayStatus, }; use rusqlite::{Connection, params}; use serde_json::json; @@ -3119,6 +3153,28 @@ mod tests { assert_eq!(row_repeat_demand.available_item_count, 1); assert_eq!(row_repeat_demand.unavailable_item_count, 1); assert_eq!(detail_repeat_demand, row_repeat_demand); + assert_eq!(buyer_order_detail.items.len(), 2); + assert!( + buyer_order_detail + .items + .iter() + .any(|item| item.line_total_minor_units == Some(1300)) + ); + assert!( + buyer_order_detail + .items + .iter() + .any(|item| item.line_total_minor_units == Some(450)) + ); + assert_eq!(buyer_order_detail.economics.total_minor_units, Some(1750)); + assert_eq!( + buyer_order_detail.economics.currency_code.as_deref(), + Some("USD") + ); + assert_eq!( + buyer_order_detail.payment, + TradePaymentDisplayStatus::NotRecorded + ); } #[test] diff --git a/crates/store/src/repo/mod.rs b/crates/store/src/repo/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod activity; pub(crate) mod buyer; pub(crate) mod farm_rules; pub(crate) mod farm_setup; +pub(crate) mod order_detail; pub(crate) mod orders; pub(crate) mod products; pub(crate) mod reminders; diff --git a/crates/store/src/repo/order_detail.rs b/crates/store/src/repo/order_detail.rs @@ -0,0 +1,90 @@ +use radroots_app_view::{OrderDetailItemRow, ProductPricePresentation, TradeEconomicsProjection}; + +use crate::AppSqliteError; + +pub(super) fn order_detail_item_row( + title: String, + quantity_display: String, + quantity_value: i64, + quantity_unit_label: String, + unit_price_minor_units: Option<u32>, + price_currency: Option<String>, +) -> Result<OrderDetailItemRow, AppSqliteError> { + let quantity = + u32::try_from(quantity_value).map_err(|_| AppSqliteError::InvalidProjection { + reason: "order detail item quantity must be non-negative", + })?; + let currency_code = price_currency + .as_deref() + .map(normalize_currency_code) + .unwrap_or_else(|| normalize_currency_code("")); + let line_total_minor_units = unit_price_minor_units + .map(|amount| { + amount + .checked_mul(quantity) + .ok_or(AppSqliteError::InvalidProjection { + reason: "order detail line total overflowed", + }) + }) + .transpose()?; + let unit_price = unit_price_minor_units.map(|amount_minor_units| ProductPricePresentation { + amount_minor_units, + currency_code, + unit_label: quantity_unit_label.trim().to_owned(), + }); + + Ok(OrderDetailItemRow { + title, + quantity_display, + unit_price, + line_total_minor_units, + }) +} + +pub(super) fn order_detail_economics( + items: &[OrderDetailItemRow], +) -> Result<TradeEconomicsProjection, AppSqliteError> { + let mut total_minor_units = 0_u32; + let mut currency_code = None::<String>; + + for item in items { + let (Some(unit_price), Some(line_total_minor_units)) = + (item.unit_price.as_ref(), item.line_total_minor_units) + else { + return Ok(TradeEconomicsProjection::default()); + }; + if let Some(existing_currency) = currency_code.as_deref() { + if existing_currency != unit_price.currency_code.as_str() { + return Ok(TradeEconomicsProjection::default()); + } + } else { + currency_code = Some(unit_price.currency_code.clone()); + } + total_minor_units = total_minor_units + .checked_add(line_total_minor_units) + .ok_or(AppSqliteError::InvalidProjection { + reason: "order detail total overflowed", + })?; + } + + Ok( + currency_code.map_or_else(TradeEconomicsProjection::default, |currency_code| { + TradeEconomicsProjection { + subtotal_minor_units: Some(total_minor_units), + discount_total_minor_units: None, + adjustment_total_minor_units: None, + total_minor_units: Some(total_minor_units), + currency_code: Some(currency_code), + } + }), + ) +} + +fn normalize_currency_code(value: &str) -> String { + let trimmed = value.trim(); + if trimmed.is_empty() { + "USD".to_owned() + } else { + trimmed.to_ascii_uppercase() + } +} diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs @@ -7,10 +7,11 @@ use radroots_app_view::{ PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow, PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, - PackDayScreenQueryState, ProductId, + PackDayScreenQueryState, ProductId, TradePaymentDisplayStatus, }; use rusqlite::{Connection, OptionalExtension, params}; +use super::order_detail::{order_detail_economics, order_detail_item_row}; use crate::AppSqliteError; pub struct AppOrdersRepository<'a> { @@ -114,6 +115,7 @@ impl<'a> AppOrdersRepository<'a> { )| { let status = parse_order_status("orders.status", status)?; let items = self.load_order_detail_items(order_id.clone())?; + let economics = order_detail_economics(&items)?; Ok(OrderDetailProjection { order_id: parse_typed_id("orders.id", order_id)?, farm_id: parse_typed_id("orders.farm_id", farm_id)?, @@ -127,6 +129,8 @@ impl<'a> AppOrdersRepository<'a> { fulfillment_window_label: empty_string_to_none(fulfillment_window_label), pickup_location_label: empty_string_to_none(pickup_location_label), items, + economics, + payment: TradePaymentDisplayStatus::NotRecorded, primary_action: primary_action_for_status(status), recoveries: Vec::new(), }) @@ -353,7 +357,13 @@ impl<'a> AppOrdersRepository<'a> { let mut statement = self .connection .prepare( - "select title, quantity_display + "select + title, + quantity_display, + quantity_value, + quantity_unit_label, + unit_price_minor_units, + price_currency from order_lines where order_id = ?1 order by sort_index asc, id asc", @@ -364,21 +374,44 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map(params![order_id], |row| { - Ok(OrderDetailItemRow { - title: row.get(0)?, - quantity_display: row.get(1)?, - }) + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, i64>(2)?, + row.get::<_, String>(3)?, + row.get::<_, Option<u32>>(4)?, + row.get::<_, Option<String>>(5)?, + )) }) .map_err(|source| AppSqliteError::Query { operation: "query order detail items", source, })?; + let mut items = Vec::new(); - rows.collect::<Result<Vec<_>, _>>() - .map_err(|source| AppSqliteError::Query { + for row in rows { + let ( + title, + quantity_display, + quantity_value, + quantity_unit_label, + unit_price_minor_units, + price_currency, + ) = row.map_err(|source| AppSqliteError::Query { operation: "read order detail items", source, - }) + })?; + items.push(order_detail_item_row( + title, + quantity_display, + quantity_value, + quantity_unit_label, + unit_price_minor_units, + price_currency, + )?); + } + + Ok(items) } fn load_seller_order_decision_lines( @@ -1219,7 +1252,7 @@ mod tests { use radroots_app_view::{ FarmId, FulfillmentWindowId, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersScreenQueryState, PackDayOutputOrderState, PackDayProductTotalRow, - PackDayScreenQueryState, PickupLocationId, + PackDayScreenQueryState, PickupLocationId, TradePaymentDisplayStatus, }; use rusqlite::{Connection, params}; @@ -1426,7 +1459,18 @@ mod tests { assert_eq!(detail.pickup_location_label.as_deref(), Some("North barn")); assert_eq!(detail.items.len(), 2); assert_eq!(detail.items[0].title, "Salad mix"); + assert_eq!( + detail.items[0] + .unit_price + .as_ref() + .map(|price| price.amount_minor_units), + Some(650) + ); + assert_eq!(detail.items[0].line_total_minor_units, Some(1300)); assert_eq!(detail.items[1].quantity_display, "1 bunch"); + assert_eq!(detail.economics.total_minor_units, Some(1950)); + assert_eq!(detail.economics.currency_code.as_deref(), Some("USD")); + assert_eq!(detail.payment, TradePaymentDisplayStatus::NotRecorded); assert_eq!(detail.primary_action, Some(OrderPrimaryAction::MarkPacked)); } @@ -1980,8 +2024,10 @@ mod tests { quantity_value, quantity_unit_label, quantity_display, + unit_price_minor_units, + price_currency, sort_index - ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + ) values (?1, ?2, ?3, ?4, ?5, ?6, 650, 'USD', ?7)", params![ line_id, order_id.to_string(), diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -1542,6 +1542,8 @@ impl OrdersListProjection { pub struct OrderDetailItemRow { pub title: String, pub quantity_display: String, + pub unit_price: Option<ProductPricePresentation>, + pub line_total_minor_units: Option<u32>, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -1555,6 +1557,8 @@ pub struct OrderDetailProjection { pub fulfillment_window_label: Option<String>, pub pickup_location_label: Option<String>, pub items: Vec<OrderDetailItemRow>, + pub economics: TradeEconomicsProjection, + pub payment: TradePaymentDisplayStatus, pub primary_action: Option<OrderPrimaryAction>, pub recoveries: Vec<OrderRecoveryProjection>, } @@ -1590,6 +1594,8 @@ pub struct BuyerOrderDetailProjection { pub fulfillment_summary: String, pub status: BuyerOrderStatus, pub items: Vec<OrderDetailItemRow>, + pub economics: TradeEconomicsProjection, + pub payment: TradePaymentDisplayStatus, pub order_note: Option<String>, pub repeat_demand: Option<RepeatDemandHandoffProjection>, } @@ -2033,10 +2039,10 @@ mod tests { SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection, ShellSection, StartupSignerEntryProjection, StartupSignerSource, StartupSignerSourceKind, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, - TradeAgreementStatus, TradeFulfillmentStatus, TradeInventoryStatus, - TradePaymentDisplayStatus, TradeProvenanceProjection, TradeReducerAgreementStatus, - TradeReducerFulfillmentStatus, TradeReducerRevisionStatus, TradeRevisionStatus, - TradeWorkflowProjection, TradeWorkflowSource, + TradeAgreementStatus, TradeEconomicsProjection, TradeFulfillmentStatus, + TradeInventoryStatus, TradePaymentDisplayStatus, TradeProvenanceProjection, + TradeReducerAgreementStatus, TradeReducerFulfillmentStatus, TradeReducerRevisionStatus, + TradeRevisionStatus, TradeWorkflowProjection, TradeWorkflowSource, }; use std::{collections::BTreeSet, str::FromStr}; use uuid::Uuid; @@ -3126,7 +3132,20 @@ mod tests { items: vec![OrderDetailItemRow { title: "Salad mix".to_owned(), quantity_display: "2 bags".to_owned(), + unit_price: Some(ProductPricePresentation { + amount_minor_units: 650, + currency_code: "USD".to_owned(), + unit_label: "bag".to_owned(), + }), + line_total_minor_units: Some(1300), }], + economics: TradeEconomicsProjection { + subtotal_minor_units: Some(1300), + total_minor_units: Some(1300), + currency_code: Some("USD".to_owned()), + ..TradeEconomicsProjection::default() + }, + payment: TradePaymentDisplayStatus::NotRecorded, primary_action: Some(OrderPrimaryAction::MarkPacked), recoveries: Vec::new(), }; @@ -3255,7 +3274,20 @@ mod tests { items: vec![OrderDetailItemRow { title: "Spring salad mix".to_owned(), quantity_display: "2 bags".to_owned(), + unit_price: Some(ProductPricePresentation { + amount_minor_units: 650, + currency_code: "USD".to_owned(), + unit_label: "bag".to_owned(), + }), + line_total_minor_units: Some(1300), }], + economics: TradeEconomicsProjection { + subtotal_minor_units: Some(1300), + total_minor_units: Some(1300), + currency_code: Some("USD".to_owned()), + ..TradeEconomicsProjection::default() + }, + payment: TradePaymentDisplayStatus::NotRecorded, order_note: Some("Leave by the cooler".to_owned()), repeat_demand: None, }; diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -116,6 +116,8 @@ "personal.orders.detail.farm.label": "Farm", "personal.orders.detail.status.label": "Status", "personal.orders.detail.fulfillment.label": "Fulfillment", + "personal.orders.detail.total.label": "Total", + "personal.orders.detail.payment.label": "Payment", "personal.orders.detail.note.label": "Order note", "personal.orders.detail.items.title": "Items", "personal.orders.repeat_demand.title": "Reorder", @@ -190,6 +192,8 @@ "orders.detail.status.label": "Status", "orders.detail.window.label": "Fulfillment window", "orders.detail.pickup.label": "Pickup location", + "orders.detail.total.label": "Total", + "orders.detail.payment.label": "Payment", "orders.recovery.section.title": "Recovery", "orders.recovery.missed_pickup.title": "Missed pickup", "orders.recovery.missed_pickup.body": "Use this when a buyer did not collect the order as planned.",