rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 13+++++++++++--
Msrc/features/trade_listing/domain/pricing.rs | 90++++++++++++++++++++++++++++---------------------------------------------------
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,