lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 5d41aecebd2dd8faa18cc0c7a87f9ad00e67f921
parent bfc315a1cb4f4c811204fd72a5f0a05a83d5692a
Author: triesap <tyson@radroots.org>
Date:   Tue,  5 May 2026 17:02:57 +0000

replica: persist exact trade product economics

- add exact economics columns for trade product rows
- project listing amount strings through replica ingest and query
- update schema models and replica db fixtures
- cover exact value preservation in replica sync tests

Diffstat:
Mcrates/replica_db/migrations/0004_trade_product.up.sql | 3+--
Acrates/replica_db/migrations/0022_trade_product_exact_economics.down.sql | 3+++
Acrates/replica_db/migrations/0022_trade_product_exact_economics.up.sql | 3+++
Mcrates/replica_db/src/migrations.rs | 5+++++
Mcrates/replica_db/src/query.rs | 7+++++--
Mcrates/replica_db/tests/error_paths.rs | 3+++
Mcrates/replica_db/tests/full_mode.rs | 6++++++
Mcrates/replica_db/tests/region_scripted_paths.rs | 3+++
Mcrates/replica_db_schema/src/models/trade_product.rs | 18++++++++++++++++++
Mcrates/replica_sync/src/ingest.rs | 39+++++++++++++++++++++++++--------------
10 files changed, 72 insertions(+), 18 deletions(-)

diff --git a/crates/replica_db/migrations/0004_trade_product.up.sql b/crates/replica_db/migrations/0004_trade_product.up.sql @@ -19,4 +19,4 @@ CREATE TABLE IF NOT EXISTS trade_product ( price_qty_amt INTEGER NOT NULL, price_qty_unit CHAR(4) NOT NULL, notes TEXT -); -\ No newline at end of file +); diff --git a/crates/replica_db/migrations/0022_trade_product_exact_economics.down.sql b/crates/replica_db/migrations/0022_trade_product_exact_economics.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE trade_product DROP COLUMN price_qty_amt_exact; +ALTER TABLE trade_product DROP COLUMN price_amt_exact; +ALTER TABLE trade_product DROP COLUMN qty_amt_exact; diff --git a/crates/replica_db/migrations/0022_trade_product_exact_economics.up.sql b/crates/replica_db/migrations/0022_trade_product_exact_economics.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE trade_product ADD COLUMN qty_amt_exact TEXT; +ALTER TABLE trade_product ADD COLUMN price_amt_exact TEXT; +ALTER TABLE trade_product ADD COLUMN price_qty_amt_exact TEXT; diff --git a/crates/replica_db/src/migrations.rs b/crates/replica_db/src/migrations.rs @@ -113,6 +113,11 @@ pub static MIGRATIONS: &[Migration] = &[ up_sql: include_str!("../migrations/0021_trade_product_primary_bin_id.up.sql"), down_sql: include_str!("../migrations/0021_trade_product_primary_bin_id.down.sql"), }, + Migration { + name: "0022_trade_product_exact_economics", + up_sql: include_str!("../migrations/0022_trade_product_exact_economics.up.sql"), + down_sql: include_str!("../migrations/0022_trade_product_exact_economics.down.sql"), + }, ]; pub fn run_all_up<E>(executor: &E) -> Result<(), SqlError> diff --git a/crates/replica_db/src/query.rs b/crates/replica_db/src/query.rs @@ -12,12 +12,15 @@ pub struct ReplicaTradeProductSummaryRow { pub title: String, pub summary: String, pub qty_amt: i64, + pub qty_amt_exact: Option<String>, pub qty_unit: String, pub qty_label: Option<String>, pub qty_avail: Option<i64>, pub price_amt: f64, + pub price_amt_exact: Option<String>, pub price_currency: String, pub price_qty_amt: u32, + pub price_qty_amt_exact: Option<String>, pub price_qty_unit: String, pub listing_addr: Option<String>, pub primary_bin_id: Option<String>, @@ -40,7 +43,7 @@ impl<E: SqlExecutor> ReplicaSql<E> { &self, lookup: &str, ) -> Result<Vec<ReplicaTradeProductSummaryRow>, SqlError> { - let sql = "SELECT tp.id, tp.key, tp.category, tp.title, tp.summary, tp.qty_amt, tp.qty_unit, tp.qty_label, tp.qty_avail, tp.price_amt, tp.price_currency, tp.price_qty_amt, tp.price_qty_unit, tp.listing_addr, tp.primary_bin_id, tp.notes, loc.location_primary \ + let sql = "SELECT tp.id, tp.key, tp.category, tp.title, tp.summary, tp.qty_amt, tp.qty_amt_exact, tp.qty_unit, tp.qty_label, tp.qty_avail, tp.price_amt, tp.price_amt_exact, tp.price_currency, tp.price_qty_amt, tp.price_qty_amt_exact, tp.price_qty_unit, tp.listing_addr, tp.primary_bin_id, tp.notes, loc.location_primary \ FROM trade_product tp \ LEFT JOIN (\ SELECT tpl.tb_tp AS trade_product_id, MIN(COALESCE(gl.label, gl.gc_name, gl.gc_admin1_name, gl.gc_country_name, gl.d_tag)) AS location_primary \ @@ -80,7 +83,7 @@ impl<E: SqlExecutor> ReplicaSql<E> { } let sql = format!( - "SELECT tp.id, tp.key, tp.category, tp.title, tp.summary, tp.qty_amt, tp.qty_unit, tp.qty_label, tp.qty_avail, tp.price_amt, tp.price_currency, tp.price_qty_amt, tp.price_qty_unit, tp.listing_addr, tp.primary_bin_id, tp.notes, loc.location_primary \ + "SELECT tp.id, tp.key, tp.category, tp.title, tp.summary, tp.qty_amt, tp.qty_amt_exact, tp.qty_unit, tp.qty_label, tp.qty_avail, tp.price_amt, tp.price_amt_exact, tp.price_currency, tp.price_qty_amt, tp.price_qty_amt_exact, tp.price_qty_unit, tp.listing_addr, tp.primary_bin_id, tp.notes, loc.location_primary \ FROM trade_product tp \ LEFT JOIN (\ SELECT tpl.tb_tp AS trade_product_id, MIN(COALESCE(gl.label, gl.gc_name, gl.gc_admin1_name, gl.gc_country_name, gl.d_tag)) AS location_primary \ diff --git a/crates/replica_db/tests/error_paths.rs b/crates/replica_db/tests/error_paths.rs @@ -914,10 +914,13 @@ fn trade_product_error_paths_cover_regions() { "profile": "floral", "year": 2024, "qty_amt": 100, + "qty_amt_exact": "100", "qty_unit": "kg", "price_amt": 7.5, + "price_amt_exact": "7.5", "price_currency": "USD", "price_qty_amt": 1, + "price_qty_amt_exact": "1", "price_qty_unit": "kg" })); assert_invalid_query(db.trade_product_create(&create_opts)); diff --git a/crates/replica_db/tests/full_mode.rs b/crates/replica_db/tests/full_mode.rs @@ -130,12 +130,15 @@ fn full_mode_shaped_query_helpers_cover_cli_reads() { "profile": "floral", "year": 2024, "qty_amt": 100, + "qty_amt_exact": "100", "qty_unit": "kg", "qty_label": "bags", "qty_avail": 2, "price_amt": 7.5, + "price_amt_exact": "7.5", "price_currency": "USD", "price_qty_amt": 1, + "price_qty_amt_exact": "1", "price_qty_unit": "kg", "listing_addr": listing_addr.clone(), "primary_bin_id": "bin-a", @@ -357,10 +360,13 @@ fn full_mode_crud_and_relation_paths() { "profile": "floral", "year": 2024, "qty_amt": 100, + "qty_amt_exact": "100", "qty_unit": "kg", "price_amt": 7.5, + "price_amt_exact": "7.5", "price_currency": "USD", "price_qty_amt": 1, + "price_qty_amt_exact": "1", "price_qty_unit": "kg" })); let trade_product_created = db diff --git a/crates/replica_db/tests/region_scripted_paths.rs b/crates/replica_db/tests/region_scripted_paths.rs @@ -722,10 +722,13 @@ assert_trade_product_paths!( "profile": "floral", "year": 2024, "qty_amt": 100, + "qty_amt_exact": "100", "qty_unit": "kg", "price_amt": 7.5, + "price_amt_exact": "7.5", "price_currency": "USD", "price_qty_amt": 1, + "price_qty_amt_exact": "1", "price_qty_unit": "kg" }), trade_product_create, diff --git a/crates/replica_db_schema/src/models/trade_product.rs b/crates/replica_db_schema/src/models/trade_product.rs @@ -19,12 +19,15 @@ pub struct TradeProduct { pub profile: String, pub year: i64, pub qty_amt: i64, + pub qty_amt_exact: Option<String>, pub qty_unit: String, pub qty_label: Option<String>, pub qty_avail: Option<i64>, pub price_amt: f64, + pub price_amt_exact: Option<String>, pub price_currency: String, pub price_qty_amt: u32, + pub price_qty_amt_exact: Option<String>, pub price_qty_unit: String, pub listing_addr: Option<String>, pub primary_bin_id: Option<String>, @@ -43,14 +46,17 @@ pub struct ITradeProductFields { pub profile: String, pub year: i64, pub qty_amt: i64, + pub qty_amt_exact: String, pub qty_unit: String, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub qty_label: Option<String>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] pub qty_avail: Option<i64>, pub price_amt: f64, + pub price_amt_exact: String, pub price_currency: String, pub price_qty_amt: u32, + pub price_qty_amt_exact: String, pub price_qty_unit: String, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub listing_addr: Option<String>, @@ -82,6 +88,8 @@ pub struct ITradeProductFieldsPartial { #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] pub qty_amt: Option<serde_json::Value>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub qty_amt_exact: Option<serde_json::Value>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub qty_unit: Option<serde_json::Value>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub qty_label: Option<serde_json::Value>, @@ -90,10 +98,14 @@ pub struct ITradeProductFieldsPartial { #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] pub price_amt: Option<serde_json::Value>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub price_amt_exact: Option<serde_json::Value>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub price_currency: Option<serde_json::Value>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] pub price_qty_amt: Option<serde_json::Value>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub price_qty_amt_exact: Option<serde_json::Value>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub price_qty_unit: Option<serde_json::Value>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub listing_addr: Option<serde_json::Value>, @@ -131,6 +143,8 @@ pub struct ITradeProductFieldsFilter { #[cfg_attr(feature = "ts-rs", ts(optional))] pub qty_amt: Option<i64>, #[cfg_attr(feature = "ts-rs", ts(optional))] + pub qty_amt_exact: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional))] pub qty_unit: Option<String>, #[cfg_attr(feature = "ts-rs", ts(optional))] pub qty_label: Option<String>, @@ -139,10 +153,14 @@ pub struct ITradeProductFieldsFilter { #[cfg_attr(feature = "ts-rs", ts(optional))] pub price_amt: Option<f64>, #[cfg_attr(feature = "ts-rs", ts(optional))] + pub price_amt_exact: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional))] pub price_currency: Option<String>, #[cfg_attr(feature = "ts-rs", ts(optional))] pub price_qty_amt: Option<u32>, #[cfg_attr(feature = "ts-rs", ts(optional))] + pub price_qty_amt_exact: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional))] pub price_qty_unit: Option<String>, #[cfg_attr(feature = "ts-rs", ts(optional))] pub listing_addr: Option<String>, diff --git a/crates/replica_sync/src/ingest.rs b/crates/replica_sync/src/ingest.rs @@ -544,27 +544,21 @@ fn trade_product_fields_from_listing( ) -> Result<ITradeProductFields, RadrootsReplicaEventsError> { let bin = primary_listing_bin(listing)?; let qty_amt = decimal_to_i64(&bin.quantity.amount, "listing primary bin quantity")?; + let qty_amt_exact = bin.quantity.amount.to_string(); let qty_avail = listing .inventory_available .as_ref() .map(|amount| decimal_to_i64(amount, "listing inventory")) .transpose()?; - let price_amt = bin + let price_source = bin .display_price .as_ref() - .unwrap_or(&bin.price_per_canonical_unit.amount) - .amount - .to_f64_lossy() - .ok_or_else(|| { - RadrootsReplicaEventsError::InvalidData("listing price amount out of range".to_string()) - })?; - let price_currency = bin - .display_price - .as_ref() - .unwrap_or(&bin.price_per_canonical_unit.amount) - .currency - .as_str() - .to_string(); + .unwrap_or(&bin.price_per_canonical_unit.amount); + let price_amt = price_source.amount.to_f64_lossy().ok_or_else(|| { + RadrootsReplicaEventsError::InvalidData("listing price amount out of range".to_string()) + })?; + let price_amt_exact = price_source.amount.to_string(); + let price_currency = price_source.currency.as_str().to_string(); let price_qty_amt = if bin.display_price.is_some() { 1 } else { @@ -573,6 +567,11 @@ fn trade_product_fields_from_listing( "listing price quantity", )? }; + let price_qty_amt_exact = if bin.display_price.is_some() { + "1".to_string() + } else { + bin.price_per_canonical_unit.quantity.amount.to_string() + }; let price_qty_unit = bin .display_price_unit .unwrap_or(bin.price_per_canonical_unit.quantity.unit) @@ -593,6 +592,7 @@ fn trade_product_fields_from_listing( .and_then(|value| value.parse::<i64>().ok()) .unwrap_or_default(), qty_amt, + qty_amt_exact, qty_unit: bin.quantity.unit.to_string(), qty_label: bin .display_label @@ -600,8 +600,10 @@ fn trade_product_fields_from_listing( .or_else(|| bin.quantity.label.clone()), qty_avail, price_amt, + price_amt_exact, price_currency, price_qty_amt, + price_qty_amt_exact, price_qty_unit, listing_addr: Some(listing_addr.to_string()), primary_bin_id: Some(listing.primary_bin_id.clone()), @@ -683,12 +685,15 @@ fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsF profile: None, year: None, qty_amt: None, + qty_amt_exact: None, qty_unit: None, qty_label: None, qty_avail: None, price_amt: None, + price_amt_exact: None, price_currency: None, price_qty_amt: None, + price_qty_amt_exact: None, price_qty_unit: None, listing_addr: Some(listing_addr.to_string()), primary_bin_id: None, @@ -768,12 +773,15 @@ fn trade_product_partial_from_fields(fields: &ITradeProductFields) -> ITradeProd profile: Some(Value::from(fields.profile.clone())), year: Some(Value::from(fields.year)), qty_amt: Some(Value::from(fields.qty_amt)), + qty_amt_exact: Some(Value::from(fields.qty_amt_exact.clone())), qty_unit: Some(Value::from(fields.qty_unit.clone())), qty_label: to_value_opt(fields.qty_label.clone()), qty_avail: fields.qty_avail.map(Value::from).or(Some(Value::Null)), price_amt: Some(Value::from(fields.price_amt)), + price_amt_exact: Some(Value::from(fields.price_amt_exact.clone())), price_currency: Some(Value::from(fields.price_currency.clone())), price_qty_amt: Some(Value::from(fields.price_qty_amt)), + price_qty_amt_exact: Some(Value::from(fields.price_qty_amt_exact.clone())), price_qty_unit: Some(Value::from(fields.price_qty_unit.clone())), listing_addr: to_value_opt(fields.listing_addr.clone()), primary_bin_id: to_value_opt(fields.primary_bin_id.clone()), @@ -2350,9 +2358,12 @@ mod tests { assert_eq!(search_rows[0].title, "Pasture Eggs"); assert_eq!(search_rows[0].primary_bin_id.as_deref(), Some("bin-a")); assert_eq!(search_rows[0].qty_amt, 12); + assert_eq!(search_rows[0].qty_amt_exact.as_deref(), Some("12")); assert_eq!(search_rows[0].qty_avail, Some(5)); assert_eq!(search_rows[0].price_amt, 6.0); + assert_eq!(search_rows[0].price_amt_exact.as_deref(), Some("6")); assert_eq!(search_rows[0].price_currency, "USD"); + assert_eq!(search_rows[0].price_qty_amt_exact.as_deref(), Some("1")); assert!( search_rows[0] .notes