commit 6e645b61b591cc3f01353ab4f44c41ba762deff7
parent 663f9e9d7486f78251c74cd26cc8a0939c1ea2bd
Author: triesap <tyson@radroots.org>
Date: Thu, 30 Apr 2026 05:54:54 +0000
order: persist quote economics
Diffstat:
4 files changed, 386 insertions(+), 7 deletions(-)
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -5,6 +5,7 @@ use std::process::ExitCode;
use radroots_events::farm::RadrootsFarm;
use radroots_events::listing::RadrootsListingLocation;
use radroots_events::profile::RadrootsProfile;
+use radroots_events::trade::RadrootsTradeOrderEconomics;
use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord;
use serde::Serialize;
@@ -1061,6 +1062,8 @@ pub struct OrderNewView {
pub ready_for_submit: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<OrderDraftItemView>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub economics: Option<RadrootsTradeOrderEconomics>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub issues: Vec<OrderIssueView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -1101,6 +1104,8 @@ pub struct OrderGetView {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<OrderDraftItemView>,
#[serde(skip_serializing_if = "Option::is_none")]
+ pub economics: Option<RadrootsTradeOrderEconomics>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_unix: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub job: Option<OrderJobView>,
@@ -1788,6 +1793,8 @@ pub struct OrderSummaryView {
#[serde(skip_serializing_if = "Option::is_none")]
pub buyer_account_id: Option<String>,
pub item_count: usize,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub economics: Option<RadrootsTradeOrderEconomics>,
pub updated_at_unix: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub job: Option<OrderJobView>,
diff --git a/src/operation_basket.rs b/src/operation_basket.rs
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
+use radroots_events::trade::RadrootsTradeOrderEconomics;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -62,8 +63,11 @@ struct BasketItem {
#[serde(deny_unknown_fields)]
struct BasketQuote {
quote_id: String,
+ quote_version: u32,
order_id: String,
order_file: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ economics: Option<RadrootsTradeOrderEconomics>,
ready_for_submit: bool,
created_at_unix: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -389,10 +393,19 @@ impl OperationService<BasketQuoteCreateRequest> for BasketOperationService<'_> {
bin_count: Some(item.quantity),
},
))?;
+ let quote_economics = order.economics.clone();
let quote = BasketQuote {
- quote_id: format!("quote_{}", loaded.document.basket.basket_id),
+ quote_id: quote_economics
+ .as_ref()
+ .map(|economics| economics.quote_id.clone())
+ .unwrap_or_else(|| format!("quote_{}", loaded.document.basket.basket_id)),
+ quote_version: quote_economics
+ .as_ref()
+ .map(|economics| economics.quote_version)
+ .unwrap_or(1),
order_id: order.order_id.clone(),
order_file: order.file.clone(),
+ economics: quote_economics,
ready_for_submit: order.ready_for_submit,
created_at_unix: now_unix(),
issues: quote_issues_from_order(&order),
@@ -599,7 +612,7 @@ fn basket_issues(document: &BasketDocument) -> Vec<BasketIssue> {
if document.basket.items.len() > 1 {
issues.push(BasketIssue {
field: "basket.items".to_owned(),
- message: "MVP basket quotes support exactly one item".to_owned(),
+ message: "basket quotes support exactly one item".to_owned(),
});
}
for item in &document.basket.items {
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -3,6 +3,10 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
+use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
+ convert_unit_decimal,
+};
use radroots_events::RadrootsNostrEventPtr;
use radroots_events::kinds::{
KIND_LISTING, KIND_TRADE_CANCEL, KIND_TRADE_FULFILLMENT_UPDATE, KIND_TRADE_ORDER_DECISION,
@@ -14,8 +18,9 @@ use radroots_events::listing::{
use radroots_events::trade::{
RadrootsActiveTradeFulfillmentState, RadrootsActiveTradeMessageType, RadrootsTradeBuyerReceipt,
RadrootsTradeFulfillmentUpdated, RadrootsTradeInventoryCommitment, RadrootsTradeOrderCancelled,
- RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderItem,
- RadrootsTradeOrderRequested,
+ RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem,
+ RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested,
+ RadrootsTradePricingBasis,
};
use radroots_events_codec::d_tag::is_d_tag_base64url;
use radroots_events_codec::listing::decode::listing_from_event;
@@ -32,11 +37,15 @@ use radroots_nostr::prelude::{
RadrootsNostrEvent, RadrootsNostrFilter, radroots_event_from_nostr, radroots_nostr_filter_tag,
radroots_nostr_kind,
};
-use radroots_replica_db::{ReplicaSql, nostr_event_state, trade_product};
+use radroots_replica_db::{
+ ReplicaSql, ReplicaTradeProductSummaryRow, nostr_event_state, trade_product,
+};
use radroots_replica_db_schema::nostr_event_state::{
INostrEventStateFindOne, INostrEventStateFindOneArgs, NostrEventStateQueryBindValues,
};
-use radroots_replica_db_schema::trade_product::{ITradeProductFieldsFilter, ITradeProductFindMany};
+use radroots_replica_db_schema::trade_product::{
+ ITradeProductFieldsFilter, ITradeProductFindMany, TradeProduct,
+};
use radroots_sql_core::SqliteExecutor;
use radroots_trade::order::{
RadrootsActiveOrderCancellationRecord, RadrootsActiveOrderDecisionRecord,
@@ -112,6 +121,8 @@ struct OrderDraft {
seller_pubkey: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
items: Vec<OrderDraftItem>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ economics: Option<RadrootsTradeOrderEconomics>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -133,6 +144,44 @@ struct ResolvedOrderListing {
listing_addr: String,
listing_event_id: String,
seller_pubkey: String,
+ economics_product: Option<ResolvedOrderEconomicsProduct>,
+}
+
+#[derive(Debug, Clone)]
+struct ResolvedOrderEconomicsProduct {
+ qty_amt: i64,
+ qty_unit: String,
+ price_amt: f64,
+ price_currency: String,
+ price_qty_amt: u32,
+ price_qty_unit: String,
+ primary_bin_id: Option<String>,
+}
+
+impl ResolvedOrderEconomicsProduct {
+ fn from_summary(row: &ReplicaTradeProductSummaryRow) -> Self {
+ Self {
+ qty_amt: row.qty_amt,
+ qty_unit: row.qty_unit.clone(),
+ price_amt: row.price_amt,
+ price_currency: row.price_currency.clone(),
+ price_qty_amt: row.price_qty_amt,
+ price_qty_unit: row.price_qty_unit.clone(),
+ primary_bin_id: row.primary_bin_id.clone(),
+ }
+ }
+
+ fn from_product(row: TradeProduct) -> Self {
+ Self {
+ qty_amt: row.qty_amt,
+ qty_unit: row.qty_unit,
+ price_amt: row.price_amt,
+ price_currency: row.price_currency,
+ price_qty_amt: row.price_qty_amt,
+ price_qty_unit: row.price_qty_unit,
+ primary_bin_id: row.primary_bin_id,
+ }
+ }
}
#[derive(Debug, Clone)]
@@ -144,6 +193,7 @@ struct ResolvedSellerOrderRequest {
buyer_pubkey: String,
seller_pubkey: String,
items: Vec<RadrootsTradeOrderItem>,
+ economics: RadrootsTradeOrderEconomics,
}
#[derive(Debug, Clone)]
@@ -229,6 +279,11 @@ pub fn scaffold(
};
let order_id = next_order_id();
+ let economics = order_economics_from_resolved_listing(
+ order_id.as_str(),
+ resolved_listing.as_ref(),
+ items.as_slice(),
+ )?;
let drafts_dir = drafts_dir(config);
fs::create_dir_all(&drafts_dir)?;
let file = drafts_dir.join(format!("{order_id}.toml"));
@@ -243,6 +298,7 @@ pub fn scaffold(
buyer_pubkey,
seller_pubkey,
items,
+ economics,
},
listing_lookup,
buyer_account_id,
@@ -306,6 +362,11 @@ pub fn scaffold_preflight(
};
let order_id = next_order_id();
+ let economics = order_economics_from_resolved_listing(
+ order_id.as_str(),
+ resolved_listing.as_ref(),
+ items.as_slice(),
+ )?;
let file = drafts_dir(config).join(format!("{order_id}.toml"));
let document = OrderDraftDocument {
version: 1,
@@ -317,6 +378,7 @@ pub fn scaffold_preflight(
buyer_pubkey,
seller_pubkey,
items,
+ economics,
},
listing_lookup,
buyer_account_id,
@@ -353,6 +415,7 @@ pub fn get(config: &RuntimeConfig, args: &RecordLookupArgs) -> Result<OrderGetVi
seller_pubkey: None,
ready_for_submit: false,
items: Vec::new(),
+ economics: None,
updated_at_unix: None,
job: None,
workflow: None,
@@ -381,6 +444,7 @@ pub fn get(config: &RuntimeConfig, args: &RecordLookupArgs) -> Result<OrderGetVi
seller_pubkey: None,
ready_for_submit: false,
items: Vec::new(),
+ economics: None,
updated_at_unix: None,
job: None,
workflow: None,
@@ -3840,6 +3904,7 @@ fn active_request_record_from_resolved(
buyer_pubkey: request.buyer_pubkey.clone(),
seller_pubkey: request.seller_pubkey.clone(),
items: request.items.clone(),
+ economics: request.economics.clone(),
},
}
}
@@ -4439,6 +4504,7 @@ fn seller_order_request_from_event(
buyer_pubkey: envelope.payload.buyer_pubkey,
seller_pubkey: envelope.payload.seller_pubkey,
items: envelope.payload.items,
+ economics: envelope.payload.economics,
})
}
@@ -4753,10 +4819,12 @@ fn resolve_order_listing(
}
let listing_event_id =
resolve_active_listing_event_id(config, listing_addr, &parsed)?.unwrap_or_default();
+ let economics_product = resolve_trade_product_by_listing_addr(config, listing_addr)?;
return Ok(Some(ResolvedOrderListing {
listing_addr: listing_addr.to_owned(),
listing_event_id,
seller_pubkey: parsed.seller_pubkey,
+ economics_product,
}));
}
@@ -4778,6 +4846,7 @@ fn resolve_order_listing(
))),
1 => {
let row = rows.into_iter().next().expect("one row");
+ let economics_product = ResolvedOrderEconomicsProduct::from_summary(&row);
let listing_addr = normalize_optional(row.listing_addr.as_deref()).ok_or_else(|| {
RuntimeError::Config(format!(
"listing `{listing_lookup}` is missing a canonical listing address; run `radroots market refresh` or pass `--listing-addr`"
@@ -4809,6 +4878,7 @@ fn resolve_order_listing(
listing_addr,
listing_event_id,
seller_pubkey: parsed.seller_pubkey,
+ economics_product: Some(economics_product),
}))
}
count => Err(RuntimeError::Config(format!(
@@ -4817,6 +4887,36 @@ fn resolve_order_listing(
}
}
+fn resolve_trade_product_by_listing_addr(
+ config: &RuntimeConfig,
+ listing_addr: &str,
+) -> Result<Option<ResolvedOrderEconomicsProduct>, RuntimeError> {
+ if !config.local.replica_db_path.exists() {
+ return Ok(None);
+ }
+
+ let executor = SqliteExecutor::open(&config.local.replica_db_path)?;
+ let product_rows = trade_product::find_many(
+ &executor,
+ &ITradeProductFindMany {
+ filter: Some(trade_product_listing_addr_filter(listing_addr)),
+ },
+ )
+ .map_err(|error| RuntimeError::Config(format!("resolve listing product state: {error:?}")))?
+ .results;
+
+ match product_rows.len() {
+ 0 => Ok(None),
+ 1 => Ok(product_rows
+ .into_iter()
+ .next()
+ .map(ResolvedOrderEconomicsProduct::from_product)),
+ count => Err(RuntimeError::Config(format!(
+ "listing address `{listing_addr}` matched {count} active local listing rows"
+ ))),
+ }
+}
+
fn resolve_active_listing_event_id(
config: &RuntimeConfig,
listing_addr: &str,
@@ -4898,6 +4998,131 @@ fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsF
}
}
+fn order_economics_from_resolved_listing(
+ order_id: &str,
+ resolved_listing: Option<&ResolvedOrderListing>,
+ items: &[OrderDraftItem],
+) -> Result<Option<RadrootsTradeOrderEconomics>, RuntimeError> {
+ let Some(listing) = resolved_listing else {
+ return Ok(None);
+ };
+ let Some(product) = listing.economics_product.as_ref() else {
+ return Ok(None);
+ };
+ let Some(primary_bin_id) = product.primary_bin_id.as_deref().and_then(non_empty_ref) else {
+ return Ok(None);
+ };
+ if items.is_empty()
+ || items
+ .iter()
+ .any(|item| item.bin_id.as_str() != primary_bin_id)
+ {
+ return Ok(None);
+ }
+
+ let currency = parse_economics_currency(product.price_currency.as_str(), "price_currency")?;
+ let quantity_amount = decimal_from_non_negative_i64(product.qty_amt, "qty_amt")?;
+ let quantity_unit = parse_economics_unit(product.qty_unit.as_str(), "qty_unit")?;
+ let price_amount = decimal_from_non_negative_f64(product.price_amt, "price_amt")?;
+ let price_quantity_amount = if product.price_qty_amt == 0 {
+ return Err(RuntimeError::Config(
+ "listing price_qty_amt must be greater than zero".to_owned(),
+ ));
+ } else {
+ RadrootsCoreDecimal::from(product.price_qty_amt)
+ };
+ let price_unit = parse_economics_unit(product.price_qty_unit.as_str(), "price_qty_unit")?;
+ let quantity_unit_in_price_units =
+ convert_unit_decimal(RadrootsCoreDecimal::ONE, quantity_unit, price_unit).map_err(
+ |error| {
+ RuntimeError::Config(format!(
+ "listing quantity unit and price unit are incompatible: {error}"
+ ))
+ },
+ )?;
+ let unit_price_amount = (price_amount / price_quantity_amount) * quantity_unit_in_price_units;
+
+ let mut subtotal_amount = RadrootsCoreDecimal::ZERO;
+ let mut economic_items = Vec::with_capacity(items.len());
+ for item in items {
+ let line_amount =
+ unit_price_amount * quantity_amount * RadrootsCoreDecimal::from(item.bin_count);
+ subtotal_amount = subtotal_amount + line_amount;
+ economic_items.push(RadrootsTradeOrderEconomicItem {
+ bin_id: item.bin_id.clone(),
+ bin_count: item.bin_count,
+ quantity_amount,
+ quantity_unit,
+ unit_price_amount,
+ unit_price_currency: currency,
+ line_subtotal: RadrootsCoreMoney::new(line_amount, currency),
+ });
+ }
+
+ let subtotal = RadrootsCoreMoney::new(subtotal_amount, currency);
+ let zero = RadrootsCoreMoney::zero(currency);
+ let mut economics = RadrootsTradeOrderEconomics {
+ quote_id: format!("quote_{order_id}"),
+ quote_version: 1,
+ pricing_basis: RadrootsTradePricingBasis::ListingEvent,
+ currency,
+ items: economic_items,
+ discounts: Vec::new(),
+ adjustments: Vec::new(),
+ subtotal: subtotal.clone(),
+ discount_total: zero.clone(),
+ adjustment_total: zero,
+ total: subtotal,
+ };
+ economics.canonicalize();
+ economics
+ .validate()
+ .map_err(|error| RuntimeError::Config(format!("build order economics: {error}")))?;
+ Ok(Some(economics))
+}
+
+fn parse_economics_currency(
+ value: &str,
+ field: &str,
+) -> Result<RadrootsCoreCurrency, RuntimeError> {
+ value
+ .parse::<RadrootsCoreCurrency>()
+ .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
+}
+
+fn parse_economics_unit(value: &str, field: &str) -> Result<RadrootsCoreUnit, RuntimeError> {
+ value
+ .parse::<RadrootsCoreUnit>()
+ .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
+}
+
+fn decimal_from_non_negative_i64(
+ value: i64,
+ field: &str,
+) -> Result<RadrootsCoreDecimal, RuntimeError> {
+ if value < 0 {
+ return Err(RuntimeError::Config(format!(
+ "listing {field} must be non-negative"
+ )));
+ }
+ Ok(RadrootsCoreDecimal::from(value))
+}
+
+fn decimal_from_non_negative_f64(
+ value: f64,
+ field: &str,
+) -> Result<RadrootsCoreDecimal, RuntimeError> {
+ if !value.is_finite() || value < 0.0 {
+ return Err(RuntimeError::Config(format!(
+ "listing {field} must be a finite non-negative decimal"
+ )));
+ }
+ value
+ .to_string()
+ .parse::<RadrootsCoreDecimal>()
+ .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
+}
+
fn view_from_loaded(loaded: LoadedOrderDraft) -> OrderGetView {
let OrderInspection {
state,
@@ -4933,6 +5158,7 @@ fn view_from_loaded(loaded: LoadedOrderDraft) -> OrderGetView {
bin_count: item.bin_count,
})
.collect(),
+ economics: loaded.document.order.economics.clone(),
updated_at_unix: Some(loaded.updated_at_unix),
job: None,
workflow: None,
@@ -4962,6 +5188,7 @@ fn summary_from_loaded(loaded: &LoadedOrderDraft) -> OrderSummaryView {
listing_event_id,
buyer_account_id: loaded.document.buyer_account_id.clone(),
item_count: loaded.document.order.items.len(),
+ economics: loaded.document.order.economics.clone(),
updated_at_unix: loaded.updated_at_unix,
job: None,
issues,
@@ -4984,6 +5211,7 @@ fn summary_for_invalid_file(path: &Path, reason: String) -> OrderSummaryView {
listing_event_id: None,
buyer_account_id: None,
item_count: 0,
+ economics: None,
updated_at_unix: modified_unix(path).unwrap_or_default(),
job: None,
issues: vec![issue_with_code("invalid_order_draft", "draft", reason)],
@@ -5100,6 +5328,27 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
}
}
+ match &document.order.economics {
+ Some(economics) => {
+ if let Err(error) = economics.validate() {
+ issues.push(issue(
+ "order.economics",
+ format!("order economics is invalid: {error}"),
+ ));
+ }
+ if !order_items_match_economics(document.order.items.as_slice(), economics) {
+ issues.push(issue(
+ "order.economics",
+ "order economics must match the order item bin ids and counts",
+ ));
+ }
+ }
+ None => issues.push(issue(
+ "order.economics",
+ "quote economics is required before order submit; run `radroots basket quote create` from current local market data",
+ )),
+ }
+
if document
.buyer_account_id
.as_deref()
@@ -5115,6 +5364,24 @@ fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
issues
}
+fn order_items_match_economics(
+ items: &[OrderDraftItem],
+ economics: &RadrootsTradeOrderEconomics,
+) -> bool {
+ let mut order_items = items
+ .iter()
+ .map(|item| (item.bin_id.as_str(), item.bin_count))
+ .collect::<Vec<_>>();
+ let mut economic_items = economics
+ .items
+ .iter()
+ .map(|item| (item.bin_id.as_str(), item.bin_count))
+ .collect::<Vec<_>>();
+ order_items.sort_unstable();
+ economic_items.sort_unstable();
+ order_items == economic_items
+}
+
fn actions_for_document(
document: &OrderDraftDocument,
file: &Path,
@@ -5761,6 +6028,10 @@ fn canonical_order_request_payload_from_loaded(
loaded: &LoadedOrderDraft,
signer_pubkey: &str,
) -> Result<RadrootsTradeOrderRequested, RuntimeError> {
+ let economics =
+ loaded.document.order.economics.clone().ok_or_else(|| {
+ RuntimeError::Config("order draft is missing quote economics".to_owned())
+ })?;
let payload = RadrootsTradeOrderRequested {
order_id: loaded.document.order.order_id.clone(),
listing_addr: loaded.document.order.listing_addr.clone(),
@@ -5776,6 +6047,7 @@ fn canonical_order_request_payload_from_loaded(
bin_count: item.bin_count,
})
.collect(),
+ economics,
};
canonicalize_active_order_request_for_signer(payload, signer_pubkey)
.map_err(|error| RuntimeError::Config(format!("canonicalize order request: {error}")))
@@ -6279,6 +6551,7 @@ impl From<OrderGetView> for OrderNewView {
seller_pubkey: view.seller_pubkey,
ready_for_submit: view.ready_for_submit,
items: view.items,
+ economics: view.economics,
issues: view.issues,
actions: view.actions,
}
@@ -6289,6 +6562,9 @@ impl From<OrderGetView> for OrderNewView {
mod tests {
use std::path::{Path, PathBuf};
+ use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
+ };
use radroots_events::RadrootsNostrEventPtr;
use radroots_events::kinds::{
KIND_TRADE_CANCEL, KIND_TRADE_FULFILLMENT_UPDATE, KIND_TRADE_ORDER_DECISION,
@@ -6298,7 +6574,9 @@ mod tests {
RadrootsActiveTradeFulfillmentState, RadrootsActiveTradeMessageType,
RadrootsTradeBuyerReceipt, RadrootsTradeFulfillmentUpdated,
RadrootsTradeInventoryCommitment, RadrootsTradeOrderCancelled, RadrootsTradeOrderDecision,
- RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderItem, RadrootsTradeOrderRequested,
+ RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem,
+ RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested,
+ RadrootsTradePricingBasis,
};
use radroots_events_codec::trade::{
active_trade_buyer_receipt_event_build, active_trade_event_context_from_tags,
@@ -6374,6 +6652,11 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: Some(sample_order_economics(
+ "ord_AAAAAAAAAAAAAAAAAAAAAg",
+ "bin-1",
+ 2,
+ )),
},
listing_lookup: Some("fresh-eggs".to_owned()),
buyer_account_id: Some("acct_demo".to_owned()),
@@ -6400,6 +6683,11 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: Some(sample_order_economics(
+ "ord_AAAAAAAAAAAAAAAAAAAAAg",
+ "bin-1",
+ 2,
+ )),
},
listing_lookup: Some("fresh-eggs".to_owned()),
buyer_account_id: Some("acct_demo".to_owned()),
@@ -6432,6 +6720,7 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: sample_order_economics("ord_AAAAAAAAAAAAAAAAAAAAAg", "bin-1", 2),
};
let parts = active_trade_order_request_event_build(
&RadrootsNostrEventPtr {
@@ -9213,6 +9502,7 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: sample_order_economics(existing_order_id, "bin-1", 2),
};
let existing_decision_payload =
accepted_order_decision_payload_from_request(&existing_request);
@@ -9308,6 +9598,7 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: sample_order_economics(existing_order_id, "bin-1", 2),
};
let existing_decision_payload =
accepted_order_decision_payload_from_request(&existing_request);
@@ -9835,6 +10126,36 @@ mod tests {
}
}
+ fn sample_order_economics(
+ order_id: &str,
+ bin_id: &str,
+ bin_count: u32,
+ ) -> RadrootsTradeOrderEconomics {
+ let currency = RadrootsCoreCurrency::USD;
+ let line_amount = RadrootsCoreDecimal::from(6u32) * RadrootsCoreDecimal::from(bin_count);
+ RadrootsTradeOrderEconomics {
+ quote_id: format!("quote_{order_id}"),
+ quote_version: 1,
+ pricing_basis: RadrootsTradePricingBasis::ListingEvent,
+ currency,
+ items: vec![RadrootsTradeOrderEconomicItem {
+ bin_id: bin_id.to_owned(),
+ bin_count,
+ quantity_amount: RadrootsCoreDecimal::ONE,
+ quantity_unit: RadrootsCoreUnit::Each,
+ unit_price_amount: RadrootsCoreDecimal::from(6u32),
+ unit_price_currency: currency,
+ line_subtotal: RadrootsCoreMoney::new(line_amount, currency),
+ }],
+ discounts: Vec::new(),
+ adjustments: Vec::new(),
+ subtotal: RadrootsCoreMoney::new(line_amount, currency),
+ discount_total: RadrootsCoreMoney::zero(currency),
+ adjustment_total: RadrootsCoreMoney::zero(currency),
+ total: RadrootsCoreMoney::new(line_amount, currency),
+ }
+ }
+
fn loaded_order_draft_for_fixture(fixture: &OrderStatusFixture) -> LoadedOrderDraft {
LoadedOrderDraft {
file: PathBuf::from(format!("{}.toml", fixture.order_id)),
@@ -9852,6 +10173,11 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: Some(sample_order_economics(
+ fixture.order_id.as_str(),
+ "bin-1",
+ 2,
+ )),
},
listing_lookup: Some("test-listing".to_owned()),
buyer_account_id: Some("acct_test".to_owned()),
@@ -10172,6 +10498,7 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: sample_order_economics(order_id, "bin-1", 2),
};
let parts = active_trade_order_request_event_build(
&RadrootsNostrEventPtr {
@@ -10204,6 +10531,7 @@ mod tests {
bin_id: "bin-1".to_owned(),
bin_count: 2,
}],
+ economics: sample_order_economics(order_id, "bin-1", 2),
};
let parts = active_trade_order_request_event_build(
&RadrootsNostrEventPtr {
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -1471,7 +1471,30 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
let order_id = quote["result"]["quote"]["order_id"]
.as_str()
.expect("order id");
+ let quote_economics = "e["result"]["quote"]["economics"];
+ let order_file = quote["result"]["order"]["file"]
+ .as_str()
+ .expect("order file");
assert_eq!(quote["result"]["quote"]["ready_for_submit"], true);
+ assert_eq!(quote["result"]["quote"]["quote_version"], 1);
+ assert_eq!(
+ quote["result"]["quote"]["quote_id"],
+ quote_economics["quote_id"]
+ );
+ assert_eq!(quote_economics["quote_version"], 1);
+ assert_eq!(quote_economics["pricing_basis"], "listing_event");
+ assert_eq!(quote_economics["currency"], "USD");
+ assert_eq!(quote_economics["items"][0]["bin_id"], "bin-1");
+ assert_eq!(quote_economics["items"][0]["bin_count"], 2);
+ assert_eq!(quote_economics["discounts"], Value::Array(Vec::new()));
+ assert_eq!(quote_economics["adjustments"], Value::Array(Vec::new()));
+ assert_eq!(
+ quote["result"]["order"]["economics"],
+ quote_economics.clone()
+ );
+ let order_draft = fs::read_to_string(order_file).expect("read order draft");
+ assert!(order_draft.contains("[order.economics]"));
+ assert!(order_draft.contains("pricing_basis = \"listing_event\""));
assert_eq!(quote["result"]["order"]["buyer_account_id"], account_id);
assert_eq!(
quote["result"]["order"]["listing_event_id"],
@@ -1492,6 +1515,10 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
orders["result"]["orders"][0]["buyer_account_id"],
account_id
);
+ assert_eq!(
+ orders["result"]["orders"][0]["economics"],
+ quote_economics.clone()
+ );
assert_eq!(orders["result"]["orders"][0]["issues"], Value::Null);
assert_no_removed_command_reference(&orders, &["order", "list"]);
assert_no_daemon_runtime_reference(&orders, &["order", "list"]);
@@ -1551,6 +1578,10 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
let order_after_submit = sandbox.json_success(&["--format", "json", "order", "get", order_id]);
assert_eq!(order_after_submit["operation_id"], "order.get");
assert_eq!(order_after_submit["result"]["state"], "ready");
+ assert_eq!(
+ order_after_submit["result"]["economics"],
+ quote_economics.clone()
+ );
assert_eq!(order_after_submit["result"]["job"], Value::Null);
assert_eq!(order_after_submit["result"]["workflow"], Value::Null);
assert_no_daemon_runtime_reference(&order_after_submit, &["order", "get"]);