app

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

commit d620a3a81a2d8a4abbd0593140f572e97f9baeb2
parent b008ee14f9815afeb347f985ac053ea26d87176c
Author: triesap <tyson@radroots.org>
Date:   Wed,  3 Jun 2026 16:29:36 -0700

view: derive workflow projection from trade reducer

Diffstat:
MCargo.lock | 3+++
Mcrates/store/src/interop.rs | 52++++++++++++++++------------------------------------
Mcrates/view/Cargo.toml | 3+++
Mcrates/view/src/lib.rs | 479+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
4 files changed, 356 insertions(+), 181 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -5189,6 +5189,9 @@ name = "radroots_app_view" version = "0.1.0" dependencies = [ "radroots_app_types", + "radroots_core", + "radroots_events", + "radroots_trade", "serde", "url", "uuid", diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs @@ -2,8 +2,9 @@ use std::{fs, path::Path}; use radroots_app_view::{ FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary, - FulfillmentWindowId, OrderId, OrderStatus, PickupLocationId, ProductId, ProductStatus, - TradeRevisionStatus, + FulfillmentWindowId, OrderId, PickupLocationId, ProductId, ProductStatus, + TradeProvenanceProjection, TradeRevisionStatus, TradeWorkflowProjection, TradeWorkflowSource, + order_status_from_active_order_projection, }; use radroots_events::{ RadrootsNostrEvent, @@ -13,8 +14,8 @@ use radroots_events::{ KIND_TRADE_RECEIPT, }, trade::{ - RadrootsActiveTradeFulfillmentState, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, - RadrootsTradeOrderRequested, RadrootsTradeOrderRevisionDecision, + RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, + RadrootsTradeOrderRevisionDecision, }, }; use radroots_events_codec::trade::{ @@ -34,7 +35,7 @@ use radroots_trade::order::{ RadrootsActiveOrderFulfillmentRecord, RadrootsActiveOrderProjection, RadrootsActiveOrderReceiptRecord, RadrootsActiveOrderRequestRecord, RadrootsActiveOrderRevisionDecisionRecord, RadrootsActiveOrderRevisionProposalRecord, - RadrootsActiveOrderStatus, reduce_active_order_events, + reduce_active_order_events, }; use rusqlite::{Connection, OptionalExtension, params}; use serde_json::Value; @@ -1181,7 +1182,13 @@ impl<'a> AppLocalInteropRepository<'a> { revision: TradeRevisionStatus, agreement_source: Option<&ActiveOrderAgreementSource>, ) -> Result<(), AppSqliteError> { - let Some(status) = order_status_from_active_projection(projection) else { + let workflow = TradeWorkflowProjection::from_active_order_projection( + order_id, + projection, + revision, + TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents), + ); + let Some(status) = order_status_from_active_order_projection(projection) else { return Ok(()); }; self.connection @@ -1192,9 +1199,9 @@ impl<'a> AppLocalInteropRepository<'a> { updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?1", params![ - order_id.to_string(), + workflow.order_id.to_string(), status.storage_key(), - revision.storage_key() + workflow.revision.storage_key() ], ) .map_err(|source| AppSqliteError::Query { @@ -1204,7 +1211,7 @@ impl<'a> AppLocalInteropRepository<'a> { if projection.economics.is_some() && let Some(agreement_source) = agreement_source { - self.replace_active_order_agreement_lines(order_id, agreement_source)?; + self.replace_active_order_agreement_lines(workflow.order_id, agreement_source)?; } Ok(()) } @@ -2814,33 +2821,6 @@ fn dedupe_active_order_evidence(evidence: &mut Vec<ActiveOrderEvidence>) { evidence.dedup_by(|left, right| left.event_id() == right.event_id()); } -fn order_status_from_active_projection( - projection: &RadrootsActiveOrderProjection, -) -> Option<OrderStatus> { - match projection.status { - RadrootsActiveOrderStatus::Missing => None, - RadrootsActiveOrderStatus::Requested => Some(OrderStatus::NeedsAction), - RadrootsActiveOrderStatus::Accepted => match projection.fulfillment_status { - Some(RadrootsActiveTradeFulfillmentState::ReadyForPickup) - | Some(RadrootsActiveTradeFulfillmentState::OutForDelivery) - | Some(RadrootsActiveTradeFulfillmentState::Delivered) => Some(OrderStatus::Packed), - Some(RadrootsActiveTradeFulfillmentState::SellerCancelled) => { - Some(OrderStatus::Declined) - } - Some(RadrootsActiveTradeFulfillmentState::Preparing) - | Some(RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled) - | None => Some(OrderStatus::Scheduled), - }, - RadrootsActiveOrderStatus::Declined | RadrootsActiveOrderStatus::Cancelled => { - Some(OrderStatus::Declined) - } - RadrootsActiveOrderStatus::Completed => Some(OrderStatus::Completed), - RadrootsActiveOrderStatus::Disputed | RadrootsActiveOrderStatus::Invalid => { - Some(OrderStatus::NeedsAction) - } - } -} - fn signed_event_projection(record: &LocalEventRecord) -> ProjectionRecord { ProjectionRecord { kind: "signed_event", diff --git a/crates/view/Cargo.toml b/crates/view/Cargo.toml @@ -8,7 +8,10 @@ license.workspace = true publish = false [dependencies] +radroots_core.workspace = true +radroots_events.workspace = true radroots_app_types.workspace = true +radroots_trade.workspace = true serde.workspace = true url = "2" diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -2,6 +2,11 @@ pub use radroots_app_types::*; +use radroots_core::RadrootsCoreMoney; +use radroots_events::trade::{RadrootsActiveTradeFulfillmentState, RadrootsTradeOrderEconomics}; +use radroots_trade::order::{ + RadrootsActiveOrderPaymentState, RadrootsActiveOrderProjection, RadrootsActiveOrderStatus, +}; use serde::{Deserialize, Serialize}; use std::{collections::BTreeSet, error::Error, fmt, str::FromStr}; use url::Url; @@ -1058,32 +1063,6 @@ pub struct BuyerCheckoutProjection { pub place_order_disabled_reason: Option<BuyerCheckoutDisabledReason>, } -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TradeReducerAgreementStatus { - Requested, - Accepted, - Declined, - Cancelled, - Completed, - Disputed, - Invalid, -} - -impl TradeReducerAgreementStatus { - pub const fn storage_key(self) -> &'static str { - match self { - Self::Requested => "requested", - Self::Accepted => "accepted", - Self::Declined => "declined", - Self::Cancelled => "cancelled", - Self::Completed => "completed", - Self::Disputed => "disputed", - Self::Invalid => "invalid", - } - } -} - #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TradeAgreementStatus { @@ -1119,43 +1098,24 @@ impl TradeAgreementStatus { } } - pub const fn from_reducer_status(status: TradeReducerAgreementStatus) -> Self { + pub const fn from_active_order_status(status: &RadrootsActiveOrderStatus) -> Self { match status { - TradeReducerAgreementStatus::Requested => Self::Ordered, - TradeReducerAgreementStatus::Accepted => Self::Confirmed, - TradeReducerAgreementStatus::Declined => Self::Declined, - TradeReducerAgreementStatus::Cancelled => Self::Cancelled, - TradeReducerAgreementStatus::Completed => Self::Completed, - TradeReducerAgreementStatus::Disputed | TradeReducerAgreementStatus::Invalid => { + RadrootsActiveOrderStatus::Missing => Self::NeedsReview, + RadrootsActiveOrderStatus::Requested => Self::Ordered, + RadrootsActiveOrderStatus::Accepted => Self::Confirmed, + RadrootsActiveOrderStatus::Declined => Self::Declined, + RadrootsActiveOrderStatus::Cancelled => Self::Cancelled, + RadrootsActiveOrderStatus::Completed => Self::Completed, + RadrootsActiveOrderStatus::Disputed | RadrootsActiveOrderStatus::Invalid => { Self::NeedsReview } } } } -impl From<TradeReducerAgreementStatus> for TradeAgreementStatus { - fn from(status: TradeReducerAgreementStatus) -> Self { - Self::from_reducer_status(status) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TradeReducerRevisionStatus { - None, - Proposed, - Accepted, - Declined, -} - -impl TradeReducerRevisionStatus { - pub const fn storage_key(self) -> &'static str { - match self { - Self::None => "none", - Self::Proposed => "proposed", - Self::Accepted => "accepted", - Self::Declined => "declined", - } +impl From<&RadrootsActiveOrderStatus> for TradeAgreementStatus { + fn from(status: &RadrootsActiveOrderStatus) -> Self { + Self::from_active_order_status(status) } } @@ -1207,15 +1167,6 @@ impl TradeRevisionStatus { } } - pub const fn from_reducer_status(status: TradeReducerRevisionStatus) -> Self { - match status { - TradeReducerRevisionStatus::None => Self::None, - TradeReducerRevisionStatus::Proposed => Self::ChangeProposed, - TradeReducerRevisionStatus::Accepted => Self::Updated, - TradeReducerRevisionStatus::Declined => Self::KeptAsPlaced, - } - } - pub fn try_from_storage_key(value: &str) -> Result<Self, ParseTradeRevisionStatusError> { match value { "none" => Ok(Self::None), @@ -1229,36 +1180,6 @@ impl TradeRevisionStatus { } } -impl From<TradeReducerRevisionStatus> for TradeRevisionStatus { - fn from(status: TradeReducerRevisionStatus) -> Self { - Self::from_reducer_status(status) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TradeReducerFulfillmentStatus { - AcceptedNotFulfilled, - Preparing, - ReadyForPickup, - OutForDelivery, - Delivered, - SellerCancelled, -} - -impl TradeReducerFulfillmentStatus { - pub const fn storage_key(self) -> &'static str { - match self { - Self::AcceptedNotFulfilled => "accepted_not_fulfilled", - Self::Preparing => "preparing", - Self::ReadyForPickup => "ready_for_pickup", - Self::OutForDelivery => "out_for_delivery", - Self::Delivered => "delivered", - Self::SellerCancelled => "seller_cancelled", - } - } -} - #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TradeFulfillmentStatus { @@ -1294,21 +1215,23 @@ impl TradeFulfillmentStatus { } } - pub const fn from_reducer_status(status: TradeReducerFulfillmentStatus) -> Self { + pub const fn from_active_fulfillment_status( + status: &RadrootsActiveTradeFulfillmentState, + ) -> Self { match status { - TradeReducerFulfillmentStatus::AcceptedNotFulfilled => Self::Confirmed, - TradeReducerFulfillmentStatus::Preparing => Self::Preparing, - TradeReducerFulfillmentStatus::ReadyForPickup => Self::ReadyForPickup, - TradeReducerFulfillmentStatus::OutForDelivery => Self::OutForDelivery, - TradeReducerFulfillmentStatus::Delivered => Self::Delivered, - TradeReducerFulfillmentStatus::SellerCancelled => Self::Cancelled, + RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled => Self::Confirmed, + RadrootsActiveTradeFulfillmentState::Preparing => Self::Preparing, + RadrootsActiveTradeFulfillmentState::ReadyForPickup => Self::ReadyForPickup, + RadrootsActiveTradeFulfillmentState::OutForDelivery => Self::OutForDelivery, + RadrootsActiveTradeFulfillmentState::Delivered => Self::Delivered, + RadrootsActiveTradeFulfillmentState::SellerCancelled => Self::Cancelled, } } } -impl From<TradeReducerFulfillmentStatus> for TradeFulfillmentStatus { - fn from(status: TradeReducerFulfillmentStatus) -> Self { - Self::from_reducer_status(status) +impl From<&RadrootsActiveTradeFulfillmentState> for TradeFulfillmentStatus { + fn from(status: &RadrootsActiveTradeFulfillmentState) -> Self { + Self::from_active_fulfillment_status(status) } } @@ -1340,6 +1263,27 @@ impl TradeInventoryStatus { Self::NeedsReview => "messages.trade.workflow.inventory.needs_review", } } + + pub fn from_active_order_projection(projection: &RadrootsActiveOrderProjection) -> Self { + match (&projection.status, projection.fulfillment_status.as_ref()) { + (RadrootsActiveOrderStatus::Requested, _) => Self::NeedsReview, + ( + RadrootsActiveOrderStatus::Accepted, + Some(RadrootsActiveTradeFulfillmentState::SellerCancelled), + ) => Self::Available, + (RadrootsActiveOrderStatus::Accepted, _) => Self::Reserved, + (RadrootsActiveOrderStatus::Declined | RadrootsActiveOrderStatus::Cancelled, _) => { + Self::Available + } + (RadrootsActiveOrderStatus::Completed, _) => Self::Reserved, + ( + RadrootsActiveOrderStatus::Missing + | RadrootsActiveOrderStatus::Disputed + | RadrootsActiveOrderStatus::Invalid, + _, + ) => Self::NeedsReview, + } + } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -1371,6 +1315,16 @@ impl TradePaymentDisplayStatus { pub const fn allows_payment_action(self) -> bool { false } + + pub fn from_active_payment_state(status: &RadrootsActiveOrderPaymentState) -> Self { + match status { + RadrootsActiveOrderPaymentState::NotRecorded => Self::NotRecorded, + RadrootsActiveOrderPaymentState::Recorded + | RadrootsActiveOrderPaymentState::Settled => Self::Recorded, + RadrootsActiveOrderPaymentState::Rejected + | RadrootsActiveOrderPaymentState::Invalid => Self::NeedsReview, + } + } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -1415,6 +1369,28 @@ pub struct TradeEconomicsProjection { pub currency_code: Option<String>, } +impl TradeEconomicsProjection { + pub fn from_trade_order_economics(economics: &RadrootsTradeOrderEconomics) -> Self { + Self { + subtotal_minor_units: money_minor_units(&economics.subtotal), + discount_total_minor_units: money_minor_units(&economics.discount_total), + adjustment_total_minor_units: money_minor_units(&economics.adjustment_total), + total_minor_units: money_minor_units(&economics.total), + currency_code: Some(economics.currency.to_string()), + } + } +} + +impl From<&RadrootsTradeOrderEconomics> for TradeEconomicsProjection { + fn from(economics: &RadrootsTradeOrderEconomics) -> Self { + Self::from_trade_order_economics(economics) + } +} + +fn money_minor_units(money: &RadrootsCoreMoney) -> Option<u32> { + money.to_minor_units_u32_exact().ok() +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct TradeProvenanceProjection { pub primary_source: TradeWorkflowSource, @@ -1439,6 +1415,11 @@ impl TradeProvenanceProjection { pub fn from_primary_source(primary_source: TradeWorkflowSource) -> Self { Self::new(primary_source, [primary_source]) } + + pub fn with_last_event_id(mut self, last_event_id: Option<String>) -> Self { + self.last_event_id = last_event_id; + self + } } impl Default for TradeProvenanceProjection { @@ -1473,11 +1454,31 @@ impl TradeWorkflowProjection { } } - pub fn from_reducer_status(order_id: OrderId, agreement: TradeReducerAgreementStatus) -> Self { - Self::new( + pub fn from_active_order_projection( + order_id: OrderId, + projection: &RadrootsActiveOrderProjection, + revision: TradeRevisionStatus, + provenance: TradeProvenanceProjection, + ) -> Self { + let mut workflow = Self::new( order_id, - TradeAgreementStatus::from_reducer_status(agreement), - ) + TradeAgreementStatus::from_active_order_status(&projection.status), + ); + workflow.revision = revision; + workflow.fulfillment = projection + .fulfillment_status + .as_ref() + .map(TradeFulfillmentStatus::from_active_fulfillment_status); + workflow.economics = projection + .economics + .as_ref() + .map(TradeEconomicsProjection::from_trade_order_economics) + .unwrap_or_default(); + workflow.inventory = TradeInventoryStatus::from_active_order_projection(projection); + workflow.payment = + TradePaymentDisplayStatus::from_active_payment_state(&projection.payment.state); + workflow.provenance = provenance.with_last_event_id(projection.last_event_id.clone()); + workflow } pub fn from_order_status(order_id: OrderId, status: OrderStatus) -> Self { @@ -1576,6 +1577,42 @@ impl TradeWorkflowProjection { } } +pub fn order_status_from_active_order_projection( + projection: &RadrootsActiveOrderProjection, +) -> Option<OrderStatus> { + match (&projection.status, projection.fulfillment_status.as_ref()) { + (RadrootsActiveOrderStatus::Missing, _) => None, + (RadrootsActiveOrderStatus::Requested, _) => Some(OrderStatus::NeedsAction), + ( + RadrootsActiveOrderStatus::Accepted, + Some( + RadrootsActiveTradeFulfillmentState::ReadyForPickup + | RadrootsActiveTradeFulfillmentState::OutForDelivery + | RadrootsActiveTradeFulfillmentState::Delivered, + ), + ) => Some(OrderStatus::Packed), + ( + RadrootsActiveOrderStatus::Accepted, + Some(RadrootsActiveTradeFulfillmentState::SellerCancelled), + ) => Some(OrderStatus::Declined), + ( + RadrootsActiveOrderStatus::Accepted, + Some( + RadrootsActiveTradeFulfillmentState::Preparing + | RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled, + ) + | None, + ) => Some(OrderStatus::Scheduled), + (RadrootsActiveOrderStatus::Declined | RadrootsActiveOrderStatus::Cancelled, _) => { + Some(OrderStatus::Declined) + } + (RadrootsActiveOrderStatus::Completed, _) => Some(OrderStatus::Completed), + (RadrootsActiveOrderStatus::Disputed | RadrootsActiveOrderStatus::Invalid, _) => { + Some(OrderStatus::NeedsAction) + } + } +} + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OrdersFilter { @@ -2135,6 +2172,19 @@ impl TodayAgendaProjection { #[cfg(test)] mod tests { + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, + }; + use radroots_events::trade::{ + RadrootsActiveTradeFulfillmentState, RadrootsTradeOrderEconomicItem, + RadrootsTradeOrderEconomics, RadrootsTradePricingBasis, + }; + use radroots_trade::order::{ + RadrootsActiveOrderPaymentProjection, RadrootsActiveOrderPaymentState, + RadrootsActiveOrderProjection, RadrootsActiveOrderSettlementState, + RadrootsActiveOrderStatus, + }; + use super::{ AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface, ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind, @@ -2171,8 +2221,8 @@ mod tests { TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, TradeAgreementStatus, TradeEconomicsProjection, TradeFulfillmentStatus, TradeInventoryStatus, TradePaymentDisplayStatus, TradeProvenanceProjection, - TradeReducerAgreementStatus, TradeReducerFulfillmentStatus, TradeReducerRevisionStatus, TradeRevisionStatus, TradeWorkflowProjection, TradeWorkflowSource, + order_status_from_active_order_projection, }; use std::{collections::BTreeSet, str::FromStr}; use uuid::Uuid; @@ -2781,47 +2831,128 @@ mod tests { ); } + fn test_decimal(raw: &str) -> RadrootsCoreDecimal { + raw.parse().expect("test decimal should parse") + } + + fn test_usd(raw: &str) -> RadrootsCoreMoney { + RadrootsCoreMoney::new(test_decimal(raw), RadrootsCoreCurrency::USD) + } + + fn test_trade_economics() -> RadrootsTradeOrderEconomics { + RadrootsTradeOrderEconomics { + quote_id: "quote-active-order".to_owned(), + quote_version: 2, + pricing_basis: RadrootsTradePricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsTradeOrderEconomicItem { + bin_id: "bin-1".to_owned(), + bin_count: 2, + quantity_amount: test_decimal("1"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: test_decimal("6.17"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: test_usd("12.34"), + }], + discounts: Vec::new(), + adjustments: Vec::new(), + subtotal: test_usd("12.34"), + discount_total: test_usd("0"), + adjustment_total: test_usd("0"), + total: test_usd("12.34"), + } + } + + fn test_payment_projection( + state: RadrootsActiveOrderPaymentState, + ) -> RadrootsActiveOrderPaymentProjection { + let mut projection = RadrootsActiveOrderPaymentProjection::not_recorded(); + projection.payment_event_id = + (!matches!(&state, RadrootsActiveOrderPaymentState::NotRecorded)) + .then(|| "payment-event-1".to_owned()); + projection.settlement_state = match &state { + RadrootsActiveOrderPaymentState::Settled => { + RadrootsActiveOrderSettlementState::Accepted + } + RadrootsActiveOrderPaymentState::Invalid => RadrootsActiveOrderSettlementState::Invalid, + _ => RadrootsActiveOrderSettlementState::NotRequired, + }; + projection.state = state; + projection + } + + fn test_active_order_projection( + status: RadrootsActiveOrderStatus, + fulfillment_status: Option<RadrootsActiveTradeFulfillmentState>, + payment_state: RadrootsActiveOrderPaymentState, + ) -> RadrootsActiveOrderProjection { + RadrootsActiveOrderProjection { + order_id: "active-order-1".to_owned(), + status, + request_event_id: Some("request-event-1".to_owned()), + decision_event_id: Some("decision-event-1".to_owned()), + fulfillment_event_id: fulfillment_status + .as_ref() + .map(|_| "fulfillment-event-1".to_owned()), + fulfillment_status, + cancellation_event_id: None, + receipt_event_id: None, + receipt_received: None, + receipt_issue: None, + receipt_received_at: None, + lifecycle_terminal: false, + payment: test_payment_projection(payment_state), + economics: Some(test_trade_economics()), + agreement_event_id: Some("decision-event-1".to_owned()), + listing_addr: Some("30402:seller:listing".to_owned()), + buyer_pubkey: Some("buyer".to_owned()), + seller_pubkey: Some("seller".to_owned()), + last_event_id: Some("fulfillment-event-1".to_owned()), + issues: Vec::new(), + } + } + #[test] - fn trade_workflow_projection_maps_reducer_status_to_product_axes() { + fn trade_workflow_projection_maps_shared_active_order_projection_to_product_axes() { assert_eq!( - TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Requested), + TradeAgreementStatus::from_active_order_status(&RadrootsActiveOrderStatus::Requested), TradeAgreementStatus::Ordered ); assert_eq!( - TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Accepted), + TradeAgreementStatus::from_active_order_status(&RadrootsActiveOrderStatus::Accepted), TradeAgreementStatus::Confirmed ); assert_eq!( - TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Disputed), + TradeAgreementStatus::from_active_order_status(&RadrootsActiveOrderStatus::Disputed), TradeAgreementStatus::NeedsReview ); assert_eq!( - TradeAgreementStatus::from_reducer_status(TradeReducerAgreementStatus::Invalid), + TradeAgreementStatus::from_active_order_status(&RadrootsActiveOrderStatus::Invalid), TradeAgreementStatus::NeedsReview ); assert_eq!( - TradeFulfillmentStatus::from_reducer_status( - TradeReducerFulfillmentStatus::AcceptedNotFulfilled + TradeFulfillmentStatus::from_active_fulfillment_status( + &RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled ), TradeFulfillmentStatus::Confirmed ); assert_eq!( - TradeFulfillmentStatus::from_reducer_status( - TradeReducerFulfillmentStatus::SellerCancelled + TradeFulfillmentStatus::from_active_fulfillment_status( + &RadrootsActiveTradeFulfillmentState::SellerCancelled ), TradeFulfillmentStatus::Cancelled ); assert_eq!( - TradeRevisionStatus::from_reducer_status(TradeReducerRevisionStatus::Proposed), - TradeRevisionStatus::ChangeProposed - ); - assert_eq!( - TradeRevisionStatus::from_reducer_status(TradeReducerRevisionStatus::Accepted), - TradeRevisionStatus::Updated + TradePaymentDisplayStatus::from_active_payment_state( + &RadrootsActiveOrderPaymentState::Settled + ), + TradePaymentDisplayStatus::Recorded ); assert_eq!( - TradeRevisionStatus::from_reducer_status(TradeReducerRevisionStatus::Declined), - TradeRevisionStatus::KeptAsPlaced + TradePaymentDisplayStatus::from_active_payment_state( + &RadrootsActiveOrderPaymentState::Rejected + ), + TradePaymentDisplayStatus::NeedsReview ); assert_eq!( TradeRevisionStatus::try_from_storage_key("none"), @@ -2841,7 +2972,7 @@ mod tests { ); assert_eq!( TradeRevisionStatus::try_from_storage_key("proposed") - .expect_err("reducer key should not parse as app revision key") + .expect_err("shared reducer key should not parse as app revision key") .value(), "proposed" ); @@ -2851,20 +2982,78 @@ mod tests { ); let order_id = OrderId::new(); - let projection = TradeWorkflowProjection::from_reducer_status( + let active_order = test_active_order_projection( + RadrootsActiveOrderStatus::Accepted, + Some(RadrootsActiveTradeFulfillmentState::ReadyForPickup), + RadrootsActiveOrderPaymentState::Recorded, + ); + let projection = TradeWorkflowProjection::from_active_order_projection( order_id, - TradeReducerAgreementStatus::Requested, + &active_order, + TradeRevisionStatus::Updated, + TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents), ); assert_eq!(projection.order_id, order_id); - assert_eq!(projection.agreement, TradeAgreementStatus::Ordered); - assert_eq!(projection.revision, TradeRevisionStatus::None); - assert_eq!(projection.fulfillment, None); - assert_eq!(projection.inventory, TradeInventoryStatus::NeedsReview); - assert_eq!(projection.payment, TradePaymentDisplayStatus::NotRecorded); + assert_eq!(projection.agreement, TradeAgreementStatus::Confirmed); + assert_eq!(projection.revision, TradeRevisionStatus::Updated); + assert_eq!( + projection.fulfillment, + Some(TradeFulfillmentStatus::ReadyForPickup) + ); + assert_eq!(projection.inventory, TradeInventoryStatus::Reserved); + assert_eq!(projection.payment, TradePaymentDisplayStatus::Recorded); + assert_eq!(projection.economics.total_minor_units, Some(1234)); + assert_eq!(projection.economics.currency_code.as_deref(), Some("USD")); assert!(!projection.payment.allows_payment_action()); assert_eq!( projection.provenance, - TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::Unknown) + TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents) + .with_last_event_id(Some("fulfillment-event-1".to_owned())) + ); + assert_eq!( + order_status_from_active_order_projection(&active_order), + Some(OrderStatus::Packed) + ); + + let requested_order = test_active_order_projection( + RadrootsActiveOrderStatus::Requested, + None, + RadrootsActiveOrderPaymentState::NotRecorded, + ); + let requested_projection = TradeWorkflowProjection::from_active_order_projection( + order_id, + &requested_order, + TradeRevisionStatus::None, + TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents), + ); + assert_eq!( + requested_projection.agreement, + TradeAgreementStatus::Ordered + ); + assert_eq!(requested_projection.fulfillment, None); + assert_eq!( + requested_projection.payment, + TradePaymentDisplayStatus::NotRecorded + ); + assert_eq!( + requested_projection.inventory, + TradeInventoryStatus::NeedsReview + ); + + let invalid_payment_order = test_active_order_projection( + RadrootsActiveOrderStatus::Accepted, + None, + RadrootsActiveOrderPaymentState::Invalid, + ); + let invalid_payment_projection = TradeWorkflowProjection::from_active_order_projection( + order_id, + &invalid_payment_order, + TradeRevisionStatus::None, + TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents), + ); + assert_eq!( + invalid_payment_projection.payment, + TradePaymentDisplayStatus::NeedsReview ); } @@ -2882,18 +3071,18 @@ mod tests { #[test] fn trade_workflow_projection_uses_localization_key_ids_for_visible_status_labels() { assert_eq!( - TradeReducerAgreementStatus::Requested.storage_key(), - "requested" - ); - assert_eq!( - TradeReducerFulfillmentStatus::AcceptedNotFulfilled.storage_key(), - "accepted_not_fulfilled" + TradeAgreementStatus::from_active_order_status(&RadrootsActiveOrderStatus::Requested) + .storage_key(), + "ordered" ); + assert_eq!(TradeAgreementStatus::Ordered.storage_key(), "ordered"); assert_eq!( - TradeReducerRevisionStatus::Proposed.storage_key(), - "proposed" + TradeFulfillmentStatus::from_active_fulfillment_status( + &RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled + ) + .storage_key(), + "confirmed" ); - assert_eq!(TradeAgreementStatus::Ordered.storage_key(), "ordered"); assert_eq!( TradeFulfillmentStatus::ReadyForPickup.storage_key(), "ready_for_pickup"