commit 5d473c5a7419a572f165656ac355f9970030b068
parent 996ab7fabc64c1f67f98122bee940fb487d7cbc1
Author: triesap <tyson@radroots.org>
Date: Sat, 28 Mar 2026 00:15:22 +0000
trade: add backoffice overlay views
Diffstat:
2 files changed, 945 insertions(+), 0 deletions(-)
diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs
@@ -3,6 +3,7 @@ pub mod dvm;
pub mod kinds;
pub mod model;
pub mod order;
+pub mod overlay;
pub mod price_ext;
pub mod projection;
pub mod tags;
diff --git a/crates/trade/src/listing/overlay.rs b/crates/trade/src/listing/overlay.rs
@@ -0,0 +1,944 @@
+#![forbid(unsafe_code)]
+
+#[cfg(not(feature = "std"))]
+use alloc::{collections::BTreeMap, string::String, vec::Vec};
+#[cfg(feature = "std")]
+use std::collections::BTreeMap;
+
+use crate::listing::projection::{
+ RadrootsTradeListingProjection, RadrootsTradeListingQuery, RadrootsTradeListingSort,
+ RadrootsTradeMarketplaceListingSummary, RadrootsTradeMarketplaceOrderSummary,
+ RadrootsTradeOrderQuery, RadrootsTradeOrderSort, RadrootsTradeOrderWorkflowProjection,
+ RadrootsTradeReadIndex,
+};
+#[cfg(feature = "ts-rs")]
+use ts_rs::TS;
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeReviewPriority {
+ Low,
+ Normal,
+ High,
+ Critical,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeReviewStatus {
+ Queued,
+ InProgress,
+ Blocked,
+ Resolved,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsTradeReviewQueueEntry {
+ pub queue: String,
+ pub priority: RadrootsTradeReviewPriority,
+ pub status: RadrootsTradeReviewStatus,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub assigned_operator: Option<String>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub reason: Option<String>,
+}
+
+impl RadrootsTradeReviewQueueEntry {
+ pub fn requires_review(&self) -> bool {
+ !matches!(self.status, RadrootsTradeReviewStatus::Resolved)
+ }
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeModerationSeverity {
+ Notice,
+ Warning,
+ Block,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeModerationStatus {
+ Open,
+ Snoozed,
+ Resolved,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsTradeModerationFlag {
+ pub code: String,
+ pub severity: RadrootsTradeModerationSeverity,
+ pub status: RadrootsTradeModerationStatus,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub source: Option<String>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub reason: Option<String>,
+}
+
+impl RadrootsTradeModerationFlag {
+ pub fn is_open(&self) -> bool {
+ !matches!(self.status, RadrootsTradeModerationStatus::Resolved)
+ }
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeFulfillmentExceptionSeverity {
+ Notice,
+ Warning,
+ Blocking,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeFulfillmentExceptionStatus {
+ Open,
+ Monitoring,
+ Resolved,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsTradeFulfillmentException {
+ pub code: String,
+ pub severity: RadrootsTradeFulfillmentExceptionSeverity,
+ pub status: RadrootsTradeFulfillmentExceptionStatus,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub source: Option<String>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub notes: Option<String>,
+}
+
+impl RadrootsTradeFulfillmentException {
+ pub fn is_open(&self) -> bool {
+ !matches!(
+ self.status,
+ RadrootsTradeFulfillmentExceptionStatus::Resolved
+ )
+ }
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsTradeListingBackofficeOverlay {
+ pub listing_addr: String,
+ #[cfg_attr(
+ feature = "ts-rs",
+ ts(optional, type = "RadrootsTradeReviewQueueEntry | null")
+ )]
+ pub review_queue: Option<RadrootsTradeReviewQueueEntry>,
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeModerationFlag[]"))]
+ pub moderation_flags: Vec<RadrootsTradeModerationFlag>,
+}
+
+impl RadrootsTradeListingBackofficeOverlay {
+ pub fn requires_review(&self) -> bool {
+ self.review_queue
+ .as_ref()
+ .is_some_and(RadrootsTradeReviewQueueEntry::requires_review)
+ }
+
+ pub fn open_moderation_flag_count(&self) -> u32 {
+ self.moderation_flags.iter().fold(0u32, |count, flag| {
+ if flag.is_open() {
+ count.saturating_add(1)
+ } else {
+ count
+ }
+ })
+ }
+
+ pub fn has_open_moderation_flags(&self) -> bool {
+ self.open_moderation_flag_count() > 0
+ }
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsTradeOrderBackofficeOverlay {
+ pub order_id: String,
+ #[cfg_attr(
+ feature = "ts-rs",
+ ts(optional, type = "RadrootsTradeReviewQueueEntry | null")
+ )]
+ pub review_queue: Option<RadrootsTradeReviewQueueEntry>,
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeModerationFlag[]"))]
+ pub moderation_flags: Vec<RadrootsTradeModerationFlag>,
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeFulfillmentException[]"))]
+ pub fulfillment_exceptions: Vec<RadrootsTradeFulfillmentException>,
+}
+
+impl RadrootsTradeOrderBackofficeOverlay {
+ pub fn requires_review(&self) -> bool {
+ self.review_queue
+ .as_ref()
+ .is_some_and(RadrootsTradeReviewQueueEntry::requires_review)
+ }
+
+ pub fn open_moderation_flag_count(&self) -> u32 {
+ self.moderation_flags.iter().fold(0u32, |count, flag| {
+ if flag.is_open() {
+ count.saturating_add(1)
+ } else {
+ count
+ }
+ })
+ }
+
+ pub fn has_open_moderation_flags(&self) -> bool {
+ self.open_moderation_flag_count() > 0
+ }
+
+ pub fn open_fulfillment_exception_count(&self) -> u32 {
+ self.fulfillment_exceptions
+ .iter()
+ .fold(0u32, |count, exception| {
+ if exception.is_open() {
+ count.saturating_add(1)
+ } else {
+ count
+ }
+ })
+ }
+
+ pub fn has_open_fulfillment_exceptions(&self) -> bool {
+ self.open_fulfillment_exception_count() > 0
+ }
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsTradeListingBackofficeQuery {
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeListingQuery"))]
+ pub listing: RadrootsTradeListingQuery,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "boolean | null"))]
+ pub requires_review: Option<bool>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "boolean | null"))]
+ pub has_open_moderation_flags: Option<bool>,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsTradeOrderBackofficeQuery {
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeOrderQuery"))]
+ pub order: RadrootsTradeOrderQuery,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "boolean | null"))]
+ pub requires_review: Option<bool>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "boolean | null"))]
+ pub has_open_moderation_flags: Option<bool>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "boolean | null"))]
+ pub has_open_fulfillment_exceptions: Option<bool>,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsTradeListingBackofficeView {
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeListingProjection"))]
+ pub listing: RadrootsTradeListingProjection,
+ #[cfg_attr(
+ feature = "ts-rs",
+ ts(optional, type = "RadrootsTradeMarketplaceListingSummary | null")
+ )]
+ pub marketplace: Option<RadrootsTradeMarketplaceListingSummary>,
+ #[cfg_attr(
+ feature = "ts-rs",
+ ts(optional, type = "RadrootsTradeListingBackofficeOverlay | null")
+ )]
+ pub overlay: Option<RadrootsTradeListingBackofficeOverlay>,
+ pub requires_review: bool,
+ pub open_moderation_flag_count: u32,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsTradeOrderBackofficeView {
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeOrderWorkflowProjection"))]
+ pub order: RadrootsTradeOrderWorkflowProjection,
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsTradeMarketplaceOrderSummary"))]
+ pub marketplace: RadrootsTradeMarketplaceOrderSummary,
+ #[cfg_attr(
+ feature = "ts-rs",
+ ts(optional, type = "RadrootsTradeOrderBackofficeOverlay | null")
+ )]
+ pub overlay: Option<RadrootsTradeOrderBackofficeOverlay>,
+ pub requires_review: bool,
+ pub open_moderation_flag_count: u32,
+ pub open_fulfillment_exception_count: u32,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct RadrootsTradeBackofficeOverlayStore {
+ listing_overlays: BTreeMap<String, RadrootsTradeListingBackofficeOverlay>,
+ order_overlays: BTreeMap<String, RadrootsTradeOrderBackofficeOverlay>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeBackofficeOverlayError {
+ MissingListingAddr,
+ MissingOrderId,
+}
+
+impl core::fmt::Display for RadrootsTradeBackofficeOverlayError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Self::MissingListingAddr => write!(f, "missing listing address"),
+ Self::MissingOrderId => write!(f, "missing order id"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsTradeBackofficeOverlayError {}
+
+impl RadrootsTradeBackofficeOverlayStore {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn listing_overlays(&self) -> &BTreeMap<String, RadrootsTradeListingBackofficeOverlay> {
+ &self.listing_overlays
+ }
+
+ pub fn order_overlays(&self) -> &BTreeMap<String, RadrootsTradeOrderBackofficeOverlay> {
+ &self.order_overlays
+ }
+
+ pub fn listing_overlay(
+ &self,
+ listing_addr: &str,
+ ) -> Option<&RadrootsTradeListingBackofficeOverlay> {
+ self.listing_overlays.get(listing_addr)
+ }
+
+ pub fn order_overlay(&self, order_id: &str) -> Option<&RadrootsTradeOrderBackofficeOverlay> {
+ self.order_overlays.get(order_id)
+ }
+
+ pub fn upsert_listing_overlay(
+ &mut self,
+ overlay: RadrootsTradeListingBackofficeOverlay,
+ ) -> Result<&RadrootsTradeListingBackofficeOverlay, RadrootsTradeBackofficeOverlayError> {
+ if overlay.listing_addr.is_empty() {
+ return Err(RadrootsTradeBackofficeOverlayError::MissingListingAddr);
+ }
+ let listing_addr = overlay.listing_addr.clone();
+ self.listing_overlays.insert(listing_addr.clone(), overlay);
+ Ok(self
+ .listing_overlays
+ .get(&listing_addr)
+ .expect("listing overlay should exist after upsert"))
+ }
+
+ pub fn upsert_order_overlay(
+ &mut self,
+ overlay: RadrootsTradeOrderBackofficeOverlay,
+ ) -> Result<&RadrootsTradeOrderBackofficeOverlay, RadrootsTradeBackofficeOverlayError> {
+ if overlay.order_id.is_empty() {
+ return Err(RadrootsTradeBackofficeOverlayError::MissingOrderId);
+ }
+ let order_id = overlay.order_id.clone();
+ self.order_overlays.insert(order_id.clone(), overlay);
+ Ok(self
+ .order_overlays
+ .get(&order_id)
+ .expect("order overlay should exist after upsert"))
+ }
+
+ pub fn merge_listing_projection(
+ &self,
+ listing: &RadrootsTradeListingProjection,
+ ) -> RadrootsTradeListingBackofficeView {
+ let overlay = self.listing_overlay(&listing.listing_addr).cloned();
+ let requires_review = overlay
+ .as_ref()
+ .is_some_and(RadrootsTradeListingBackofficeOverlay::requires_review);
+ let open_moderation_flag_count = overlay.as_ref().map_or(
+ 0,
+ RadrootsTradeListingBackofficeOverlay::open_moderation_flag_count,
+ );
+
+ RadrootsTradeListingBackofficeView {
+ listing: listing.clone(),
+ marketplace: listing.marketplace_summary(),
+ overlay,
+ requires_review,
+ open_moderation_flag_count,
+ }
+ }
+
+ pub fn merge_order_projection(
+ &self,
+ order: &RadrootsTradeOrderWorkflowProjection,
+ ) -> RadrootsTradeOrderBackofficeView {
+ let overlay = self.order_overlay(&order.order_id).cloned();
+ let requires_review = overlay
+ .as_ref()
+ .is_some_and(RadrootsTradeOrderBackofficeOverlay::requires_review);
+ let open_moderation_flag_count = overlay.as_ref().map_or(
+ 0,
+ RadrootsTradeOrderBackofficeOverlay::open_moderation_flag_count,
+ );
+ let open_fulfillment_exception_count = overlay.as_ref().map_or(
+ 0,
+ RadrootsTradeOrderBackofficeOverlay::open_fulfillment_exception_count,
+ );
+
+ RadrootsTradeOrderBackofficeView {
+ order: order.clone(),
+ marketplace: order.marketplace_summary(),
+ overlay,
+ requires_review,
+ open_moderation_flag_count,
+ open_fulfillment_exception_count,
+ }
+ }
+
+ pub fn listing_backoffice_views(
+ &self,
+ read_index: &RadrootsTradeReadIndex,
+ query: &RadrootsTradeListingBackofficeQuery,
+ sort: RadrootsTradeListingSort,
+ ) -> Vec<RadrootsTradeListingBackofficeView> {
+ read_index
+ .query_listings(&query.listing, sort)
+ .into_iter()
+ .map(|listing| self.merge_listing_projection(listing))
+ .filter(|view| listing_backoffice_matches_query(view, query))
+ .collect()
+ }
+
+ pub fn order_backoffice_views(
+ &self,
+ read_index: &RadrootsTradeReadIndex,
+ query: &RadrootsTradeOrderBackofficeQuery,
+ sort: RadrootsTradeOrderSort,
+ ) -> Vec<RadrootsTradeOrderBackofficeView> {
+ read_index
+ .query_orders(&query.order, sort)
+ .into_iter()
+ .map(|order| self.merge_order_projection(order))
+ .filter(|view| order_backoffice_matches_query(view, query))
+ .collect()
+ }
+}
+
+fn bool_filter_matches(value: bool, filter: Option<bool>) -> bool {
+ match filter {
+ Some(expected) => expected == value,
+ None => true,
+ }
+}
+
+fn listing_backoffice_matches_query(
+ view: &RadrootsTradeListingBackofficeView,
+ query: &RadrootsTradeListingBackofficeQuery,
+) -> bool {
+ bool_filter_matches(view.requires_review, query.requires_review)
+ && bool_filter_matches(
+ view.open_moderation_flag_count > 0,
+ query.has_open_moderation_flags,
+ )
+}
+
+fn order_backoffice_matches_query(
+ view: &RadrootsTradeOrderBackofficeView,
+ query: &RadrootsTradeOrderBackofficeQuery,
+) -> bool {
+ bool_filter_matches(view.requires_review, query.requires_review)
+ && bool_filter_matches(
+ view.open_moderation_flag_count > 0,
+ query.has_open_moderation_flags,
+ )
+ && bool_filter_matches(
+ view.open_fulfillment_exception_count > 0,
+ query.has_open_fulfillment_exceptions,
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ RadrootsTradeBackofficeOverlayError, RadrootsTradeBackofficeOverlayStore,
+ RadrootsTradeFulfillmentException, RadrootsTradeFulfillmentExceptionSeverity,
+ RadrootsTradeFulfillmentExceptionStatus, RadrootsTradeListingBackofficeOverlay,
+ RadrootsTradeListingBackofficeQuery, RadrootsTradeModerationFlag,
+ RadrootsTradeModerationSeverity, RadrootsTradeModerationStatus,
+ RadrootsTradeOrderBackofficeOverlay, RadrootsTradeOrderBackofficeQuery,
+ RadrootsTradeReviewPriority, RadrootsTradeReviewQueueEntry, RadrootsTradeReviewStatus,
+ };
+ use crate::listing::{
+ dvm::{TradeListingMessagePayload, TradeOrderResponse},
+ projection::RadrootsTradeOrderWorkflowMessage,
+ };
+ use crate::listing::{
+ order::{
+ TradeFulfillmentStatus, TradeFulfillmentUpdate, TradeOrder, TradeOrderItem,
+ TradeOrderStatus, TradeReceipt,
+ },
+ projection::{
+ RadrootsTradeListingSort, RadrootsTradeListingSortField, RadrootsTradeOrderQuery,
+ RadrootsTradeOrderSort, RadrootsTradeOrderSortField, RadrootsTradeReadIndex,
+ RadrootsTradeSortDirection,
+ },
+ };
+ use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCorePercent,
+ RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit,
+ };
+ use radroots_events::listing::{
+ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
+ RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation,
+ RadrootsListingProduct, RadrootsListingStatus,
+ };
+
+ fn base_listing() -> RadrootsListing {
+ RadrootsListing {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(),
+ farm: RadrootsListingFarmRef {
+ pubkey: "farm-pubkey".into(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(),
+ },
+ product: RadrootsListingProduct {
+ key: "coffee".into(),
+ title: "Coffee".into(),
+ category: "coffee".into(),
+ summary: Some("single origin".into()),
+ process: None,
+ lot: None,
+ location: None,
+ profile: None,
+ year: None,
+ },
+ primary_bin_id: "bin-1".into(),
+ bins: vec![RadrootsListingBin {
+ bin_id: "bin-1".into(),
+ quantity: RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(1000u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ price_per_canonical_unit: RadrootsCoreQuantityPrice::new(
+ RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(2u32),
+ RadrootsCoreCurrency::USD,
+ ),
+ RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(1u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ ),
+ display_amount: None,
+ display_unit: None,
+ display_label: Some("1kg bag".into()),
+ display_price: Some(RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(2000u32),
+ RadrootsCoreCurrency::USD,
+ )),
+ display_price_unit: Some(RadrootsCoreUnit::Each),
+ }],
+ resource_area: None,
+ plot: None,
+ discounts: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(10u32)),
+ availability: Some(RadrootsListingAvailability::Status {
+ status: RadrootsListingStatus::Active,
+ }),
+ delivery_method: Some(RadrootsListingDeliveryMethod::Pickup),
+ location: Some(RadrootsListingLocation {
+ primary: "farm".into(),
+ city: Some("Nashville".into()),
+ region: Some("TN".into()),
+ country: Some("US".into()),
+ lat: None,
+ lng: None,
+ geohash: None,
+ }),
+ images: None,
+ }
+ }
+
+ fn alternate_listing() -> RadrootsListing {
+ RadrootsListing {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".into(),
+ farm: RadrootsListingFarmRef {
+ pubkey: "farm-pubkey-2".into(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAABA".into(),
+ },
+ product: RadrootsListingProduct {
+ key: "greens".into(),
+ title: "Greens".into(),
+ category: "vegetables".into(),
+ summary: Some("washed bunches".into()),
+ process: None,
+ lot: None,
+ location: None,
+ profile: None,
+ year: None,
+ },
+ primary_bin_id: "bin-1".into(),
+ bins: vec![RadrootsListingBin {
+ bin_id: "bin-1".into(),
+ quantity: RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(500u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ price_per_canonical_unit: RadrootsCoreQuantityPrice::new(
+ RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(4u32),
+ RadrootsCoreCurrency::USD,
+ ),
+ RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(1u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ ),
+ display_amount: None,
+ display_unit: None,
+ display_label: Some("500g bunch".into()),
+ display_price: Some(RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(2000u32),
+ RadrootsCoreCurrency::USD,
+ )),
+ display_price_unit: Some(RadrootsCoreUnit::Each),
+ }],
+ resource_area: None,
+ plot: None,
+ discounts: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(4u32)),
+ availability: Some(RadrootsListingAvailability::Status {
+ status: RadrootsListingStatus::Sold,
+ }),
+ delivery_method: Some(RadrootsListingDeliveryMethod::Shipping),
+ location: Some(RadrootsListingLocation {
+ primary: "warehouse".into(),
+ city: Some("Louisville".into()),
+ region: Some("KY".into()),
+ country: Some("US".into()),
+ lat: None,
+ lng: None,
+ geohash: None,
+ }),
+ images: None,
+ }
+ }
+
+ fn base_order() -> TradeOrder {
+ TradeOrder {
+ order_id: "order-1".into(),
+ listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(),
+ buyer_pubkey: "buyer-pubkey".into(),
+ seller_pubkey: "seller-pubkey".into(),
+ items: vec![TradeOrderItem {
+ bin_id: "bin-1".into(),
+ bin_count: 2,
+ }],
+ discounts: Some(vec![radroots_core::RadrootsCoreDiscountValue::Percent(
+ RadrootsCorePercent::new(RadrootsCoreDecimal::from(10u32)),
+ )]),
+ notes: Some("deliver friday".into()),
+ status: TradeOrderStatus::Draft,
+ }
+ }
+
+ fn alternate_order() -> TradeOrder {
+ TradeOrder {
+ order_id: "order-2".into(),
+ listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw".into(),
+ buyer_pubkey: "buyer-pubkey-2".into(),
+ seller_pubkey: "seller-pubkey".into(),
+ items: vec![TradeOrderItem {
+ bin_id: "bin-1".into(),
+ bin_count: 3,
+ }],
+ discounts: None,
+ notes: Some("expedite".into()),
+ status: TradeOrderStatus::Draft,
+ }
+ }
+
+ fn message(
+ actor_pubkey: &str,
+ listing_addr: &str,
+ order_id: Option<&str>,
+ payload: TradeListingMessagePayload,
+ ) -> RadrootsTradeOrderWorkflowMessage {
+ RadrootsTradeOrderWorkflowMessage {
+ actor_pubkey: actor_pubkey.into(),
+ listing_addr: listing_addr.into(),
+ order_id: order_id.map(str::to_string),
+ payload,
+ }
+ }
+
+ #[test]
+ fn listing_backoffice_views_merge_overlay_without_mutating_canonical_projection() {
+ let mut index = RadrootsTradeReadIndex::new();
+ index
+ .upsert_listing("seller-pubkey", &base_listing())
+ .expect("base listing");
+ index
+ .upsert_listing("seller-pubkey", &alternate_listing())
+ .expect("alternate listing");
+
+ let mut overlays = RadrootsTradeBackofficeOverlayStore::new();
+ overlays
+ .upsert_listing_overlay(RadrootsTradeListingBackofficeOverlay {
+ listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(),
+ review_queue: Some(RadrootsTradeReviewQueueEntry {
+ queue: "listing-review".into(),
+ priority: RadrootsTradeReviewPriority::High,
+ status: RadrootsTradeReviewStatus::Queued,
+ assigned_operator: Some("ops-1".into()),
+ reason: Some("verify organic claim".into()),
+ }),
+ moderation_flags: vec![RadrootsTradeModerationFlag {
+ code: "needs-copy-review".into(),
+ severity: RadrootsTradeModerationSeverity::Warning,
+ status: RadrootsTradeModerationStatus::Open,
+ source: Some("policy".into()),
+ reason: Some("contains superlative marketing copy".into()),
+ }],
+ })
+ .expect("listing overlay");
+
+ let views = overlays.listing_backoffice_views(
+ &index,
+ &RadrootsTradeListingBackofficeQuery {
+ has_open_moderation_flags: Some(true),
+ ..Default::default()
+ },
+ RadrootsTradeListingSort {
+ field: RadrootsTradeListingSortField::ListingAddr,
+ direction: RadrootsTradeSortDirection::Asc,
+ },
+ );
+
+ assert_eq!(views.len(), 1);
+ assert_eq!(views[0].listing.listing_addr, base_order().listing_addr);
+ assert!(views[0].requires_review);
+ assert_eq!(views[0].open_moderation_flag_count, 1);
+ assert_eq!(
+ views[0]
+ .overlay
+ .as_ref()
+ .and_then(|overlay| overlay.review_queue.as_ref())
+ .and_then(|entry| entry.assigned_operator.as_deref()),
+ Some("ops-1")
+ );
+
+ let listing = index
+ .listing("30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg")
+ .expect("canonical listing");
+ assert_eq!(listing.order_count, 0);
+ assert_eq!(listing.open_order_count, 0);
+ assert_eq!(listing.terminal_order_count, 0);
+ }
+
+ #[test]
+ fn order_backoffice_views_filter_review_and_exception_overlays() {
+ let mut index = RadrootsTradeReadIndex::new();
+ index
+ .upsert_listing("seller-pubkey", &base_listing())
+ .expect("base listing");
+ index
+ .upsert_listing("seller-pubkey", &alternate_listing())
+ .expect("alternate listing");
+
+ index
+ .apply_workflow_message(&message(
+ "buyer-pubkey",
+ "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg",
+ Some("order-1"),
+ TradeListingMessagePayload::OrderRequest(base_order()),
+ ))
+ .expect("first order");
+ index
+ .apply_workflow_message(&message(
+ "buyer-pubkey-2",
+ "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw",
+ Some("order-2"),
+ TradeListingMessagePayload::OrderRequest(alternate_order()),
+ ))
+ .expect("second order");
+ index
+ .apply_workflow_message(&message(
+ "seller-pubkey",
+ "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw",
+ Some("order-2"),
+ TradeListingMessagePayload::OrderResponse(TradeOrderResponse {
+ accepted: true,
+ reason: Some("approved".into()),
+ }),
+ ))
+ .expect("accepted");
+ index
+ .apply_workflow_message(&message(
+ "seller-pubkey",
+ "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw",
+ Some("order-2"),
+ TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate {
+ status: TradeFulfillmentStatus::Delivered,
+ tracking: Some("track-2".into()),
+ eta: None,
+ notes: Some("in transit".into()),
+ }),
+ ))
+ .expect("fulfilled");
+ index
+ .apply_workflow_message(&message(
+ "buyer-pubkey-2",
+ "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw",
+ Some("order-2"),
+ TradeListingMessagePayload::Receipt(TradeReceipt {
+ acknowledged: true,
+ at: 1_700_000_020,
+ note: Some("all good".into()),
+ }),
+ ))
+ .expect("completed");
+
+ let mut overlays = RadrootsTradeBackofficeOverlayStore::new();
+ overlays
+ .upsert_order_overlay(RadrootsTradeOrderBackofficeOverlay {
+ order_id: "order-1".into(),
+ review_queue: Some(RadrootsTradeReviewQueueEntry {
+ queue: "order-review".into(),
+ priority: RadrootsTradeReviewPriority::Critical,
+ status: RadrootsTradeReviewStatus::InProgress,
+ assigned_operator: Some("ops-2".into()),
+ reason: Some("buyer requested rush handling".into()),
+ }),
+ moderation_flags: vec![RadrootsTradeModerationFlag {
+ code: "buyer-note-review".into(),
+ severity: RadrootsTradeModerationSeverity::Notice,
+ status: RadrootsTradeModerationStatus::Snoozed,
+ source: Some("operator".into()),
+ reason: Some("monitor communication tone".into()),
+ }],
+ fulfillment_exceptions: vec![RadrootsTradeFulfillmentException {
+ code: "dock-delay".into(),
+ severity: RadrootsTradeFulfillmentExceptionSeverity::Blocking,
+ status: RadrootsTradeFulfillmentExceptionStatus::Open,
+ source: Some("fulfillment".into()),
+ notes: Some("carrier missed pickup window".into()),
+ }],
+ })
+ .expect("order overlay");
+ overlays
+ .upsert_order_overlay(RadrootsTradeOrderBackofficeOverlay {
+ order_id: "order-2".into(),
+ review_queue: Some(RadrootsTradeReviewQueueEntry {
+ queue: "order-review".into(),
+ priority: RadrootsTradeReviewPriority::Low,
+ status: RadrootsTradeReviewStatus::Resolved,
+ assigned_operator: None,
+ reason: Some("completed successfully".into()),
+ }),
+ moderation_flags: Vec::new(),
+ fulfillment_exceptions: vec![RadrootsTradeFulfillmentException {
+ code: "tracking-delay".into(),
+ severity: RadrootsTradeFulfillmentExceptionSeverity::Notice,
+ status: RadrootsTradeFulfillmentExceptionStatus::Resolved,
+ source: Some("fulfillment".into()),
+ notes: Some("carrier synced late".into()),
+ }],
+ })
+ .expect("resolved order overlay");
+
+ let views = overlays.order_backoffice_views(
+ &index,
+ &RadrootsTradeOrderBackofficeQuery {
+ order: RadrootsTradeOrderQuery {
+ seller_pubkey: Some("seller-pubkey".into()),
+ ..Default::default()
+ },
+ requires_review: Some(true),
+ has_open_fulfillment_exceptions: Some(true),
+ ..Default::default()
+ },
+ RadrootsTradeOrderSort {
+ field: RadrootsTradeOrderSortField::OrderId,
+ direction: RadrootsTradeSortDirection::Asc,
+ },
+ );
+
+ assert_eq!(views.len(), 1);
+ assert_eq!(views[0].order.order_id, "order-1");
+ assert_eq!(views[0].open_moderation_flag_count, 1);
+ assert_eq!(views[0].open_fulfillment_exception_count, 1);
+ assert!(views[0].requires_review);
+ assert_eq!(views[0].marketplace.status, TradeOrderStatus::Requested);
+
+ let completed_order = index.order("order-2").expect("canonical completed order");
+ assert_eq!(completed_order.status, TradeOrderStatus::Completed);
+ assert_eq!(completed_order.receipt_count, 1);
+ }
+
+ #[test]
+ fn overlay_store_rejects_missing_identity_keys() {
+ let mut overlays = RadrootsTradeBackofficeOverlayStore::new();
+
+ let listing_err = overlays
+ .upsert_listing_overlay(RadrootsTradeListingBackofficeOverlay {
+ listing_addr: String::new(),
+ review_queue: None,
+ moderation_flags: Vec::new(),
+ })
+ .expect_err("missing listing addr should fail");
+ assert_eq!(
+ listing_err,
+ RadrootsTradeBackofficeOverlayError::MissingListingAddr
+ );
+
+ let order_err = overlays
+ .upsert_order_overlay(RadrootsTradeOrderBackofficeOverlay {
+ order_id: String::new(),
+ review_queue: None,
+ moderation_flags: Vec::new(),
+ fulfillment_exceptions: Vec::new(),
+ })
+ .expect_err("missing order id should fail");
+ assert_eq!(
+ order_err,
+ RadrootsTradeBackofficeOverlayError::MissingOrderId
+ );
+ }
+}