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:
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