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:
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.",