commit 3b3bc3b87e137391a9825fdb9ffbd0f41c522c1a
parent 8f7dc600e8603d64d6a031e5897adfc4070c0b86
Author: triesap <triesap@radroots.dev>
Date: Sat, 3 Jan 2026 22:18:02 +0000
trade: validate bin inputs and use bin pricing helpers
- Add radroots-log crate and wire into workspace deps
- Replace quantity-based pricing with bin lookup and pricing ext
- Validate non-empty bin_id and positive bin_count before pricing
- Improve unsatisfiable errors for missing bin and pricing failures
Diffstat:
2 files changed, 43 insertions(+), 60 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1815,6 +1815,16 @@ dependencies = [
]
[[package]]
+name = "radroots-log"
+version = "0.1.0"
+dependencies = [
+ "thiserror 1.0.69",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "radroots-nostr"
version = "0.1.0"
dependencies = [
@@ -1836,6 +1846,7 @@ dependencies = [
"anyhow",
"clap",
"config",
+ "radroots-log",
"serde",
"serde_json",
"tempfile",
@@ -1843,8 +1854,6 @@ dependencies = [
"tokio",
"toml",
"tracing",
- "tracing-appender",
- "tracing-subscriber",
]
[[package]]
diff --git a/src/features/trade_listing/domain/pricing.rs b/src/features/trade_listing/domain/pricing.rs
@@ -1,8 +1,5 @@
-use radroots_core::{RadrootsCoreQuantity, RadrootsCoreQuantityPrice};
-use radroots_events::listing::{
- RadrootsListing, RadrootsListingDiscount, RadrootsListingQuantity,
-};
-use radroots_trade::prelude::price_ext::ListingPricingExt;
+use radroots_events::listing::RadrootsListing;
+use radroots_trade::prelude::price_ext::BinPricingTryExt;
use radroots_trade::prelude::stage::order::{
TradeListingOrderRequestPayload, TradeListingOrderResult,
};
@@ -21,73 +18,50 @@ impl ListingOrderCalculator for RadrootsListing {
&self,
order: &TradeListingOrderRequestPayload,
) -> Result<TradeListingOrderResult, JobRequestOrderError> {
- let req_qty: &RadrootsListingQuantity = &order.quantity;
- let req_qty_amount = req_qty.value.amount;
- let req_qty_unit = req_qty.value.unit;
- let req_qty_label_opt = req_qty.label.as_deref();
-
- let matched_packaging = self.quantities.iter().any(|q| {
- let same_amount = q.value.amount.normalize() == req_qty_amount.normalize();
- let same_unit = q.value.unit == req_qty_unit;
- let label_ok = match (q.label.as_deref(), req_qty_label_opt) {
- (Some(l), Some(r)) => l == r,
- (None, None) => true,
- _ => false,
- };
- same_amount && same_unit && label_ok
- });
-
- if !matched_packaging {
+ if order.bin_id.trim().is_empty() {
return Err(JobRequestOrderError::Unsatisfiable(format!(
- "requested packaging {} {} {} not available",
- req_qty_amount,
- req_qty_unit,
- req_qty_label_opt.unwrap_or("")
+ "requested bin id is empty"
)));
}
- let req_money = order.price.amount.clone().quantize_to_currency();
+ if order.bin_count == 0 {
+ return Err(JobRequestOrderError::Unsatisfiable(
+ "requested bin count must be greater than 0".to_string(),
+ ));
+ }
- let matched_tier: &RadrootsCoreQuantityPrice = self
- .prices
+ let bin = self
+ .bins
.iter()
- .find(|p| {
- let money_ok = p.amount.currency == req_money.currency
- && p.amount.amount.normalize() == req_money.amount.normalize();
- let per_amt_ok =
- p.quantity.amount.normalize() == order.price.quantity.amount.normalize();
- let per_unit_ok = p.quantity.unit == order.price.quantity.unit;
- money_ok && per_amt_ok && per_unit_ok
- })
+ .find(|bin| bin.bin_id == order.bin_id)
.ok_or_else(|| {
JobRequestOrderError::Unsatisfiable(format!(
- "no matching price tier {} {} found",
- order.price.quantity.amount, order.price.quantity.unit
+ "requested bin {} not available",
+ order.bin_id
))
})?;
- let price_amount = matched_tier.amount.clone();
- let price_quantity = matched_tier.quantity.clone();
-
- let discounts_out: Vec<RadrootsListingDiscount> =
- self.discounts.clone().unwrap_or_default();
-
- let out_quantity = RadrootsListingQuantity {
- value: RadrootsCoreQuantity::new(req_qty_amount, req_qty_unit),
- label: req_qty.label.clone(),
- count: req_qty.count,
- };
-
- let out_price = RadrootsCoreQuantityPrice {
- amount: price_amount.clone(),
- quantity: price_quantity.clone(),
- };
+ let out_price = bin.price_per_canonical_unit.clone();
+ let out_subtotal = bin
+ .try_subtotal_for_count(order.bin_count)
+ .map_err(|err| {
+ JobRequestOrderError::Unsatisfiable(format!(
+ "failed to price requested bin: {err}"
+ ))
+ })?;
+ let out_total = bin
+ .try_total_for_count(order.bin_count)
+ .map_err(|err| {
+ JobRequestOrderError::Unsatisfiable(format!(
+ "failed to total requested bin: {err}"
+ ))
+ })?;
- let out_subtotal = out_price.subtotal_for(&out_quantity);
- let out_total = out_price.total_for(&out_quantity);
+ let discounts_out = self.discounts.clone().unwrap_or_default();
Ok(TradeListingOrderResult {
- quantity: out_quantity,
+ bin_id: order.bin_id.clone(),
+ bin_count: order.bin_count,
price: out_price,
discounts: discounts_out,
subtotal: out_subtotal,