commit e8759939caa98be78c342ffd6bde7008efe6235a
parent 4f67a9eeffa497f427b1dd0bd70435db81a82641
Author: triesap <tyson@radroots.org>
Date: Sun, 24 May 2026 17:56:11 +0000
app: preserve buyer order bin identity
- add sqlite listing-bin identity snapshots for imported products, carts, and order lines
- export app buyer order items and economics with the selected listing bin id
- prove non-bin-1 app order export survives a later listing projection change
- validate schema, focused buyer/import/runtime paths, full cargo test, formatting, and diff hygiene
Diffstat:
6 files changed, 327 insertions(+), 63 deletions(-)
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -4194,8 +4194,16 @@ impl AppBuyerOrderRequestExport {
let mut order_items = Vec::with_capacity(order.lines.len());
let mut line_refs = Vec::with_capacity(order.lines.len());
for line in &order.lines {
+ let line_bin_id = line
+ .listing_bin_id
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty());
+ if line_bin_id.is_none() && !support_issues.contains(&"listing_bin_id_required") {
+ support_issues.push("listing_bin_id_required");
+ }
order_items.push(json!({
- "bin_id": "bin-1",
+ "bin_id": line_bin_id.unwrap_or_default(),
"bin_count": line.quantity,
}));
line_refs.push(json!({
@@ -4208,6 +4216,7 @@ impl AppBuyerOrderRequestExport {
},
"listing_addr": line.listing_addr,
"listing_event_id": line.listing_event_id,
+ "listing_bin_id": line.listing_bin_id,
"seller_pubkey": line.seller_pubkey,
"farm_key": line.farm_key,
}));
@@ -4322,6 +4331,15 @@ fn order_economics_json(
let mut currency = None::<String>;
for line in &order.lines {
+ let line_bin_id = line
+ .listing_bin_id
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty());
+ if line_bin_id.is_none() && !support_issues.contains(&"listing_bin_id_required") {
+ support_issues.push("listing_bin_id_required");
+ continue;
+ }
let Some(quantity_unit) = canonical_quantity_unit(line.quantity_unit_label.as_str()) else {
support_issues.push("canonical_quantity_unit_required");
continue;
@@ -4359,7 +4377,7 @@ fn order_economics_json(
reason: "buyer order local event subtotal overflowed",
})?;
economics_items.push(json!({
- "bin_id": "bin-1",
+ "bin_id": line_bin_id.unwrap_or_default(),
"bin_count": line.quantity,
"quantity_amount": "1",
"quantity_unit": quantity_unit,
@@ -8475,12 +8493,13 @@ mod tests {
.account_id
.clone();
let listing_key = "DDDDDDDDDDDDDDDDDDDDDD";
- append_cli_signed_buyer_listing_record_with(
+ append_cli_signed_buyer_listing_record_with_bin(
&paths,
"buyer-order-supported-listing",
listing_key,
"Buyer Visible Eggs",
1100,
+ "dozen-eggs",
);
let product_id =
deterministic_cli_listing_product_id(Some("buyer-visible-seller-pubkey"), listing_key);
@@ -8496,6 +8515,17 @@ mod tests {
.add_personal_product_to_cart(PersonalSection::Browse, false)
.expect("buyer product should add to cart")
);
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .connection()
+ .execute(
+ "update products set listing_bin_id = 'mutated-bin' where id = ?1",
+ [product_id.to_string()],
+ )
+ .expect("listing projection should mutate after cart snapshot");
assert!(
runtime
.save_personal_checkout_draft(BuyerCheckoutDraft {
@@ -8603,13 +8633,20 @@ mod tests {
payload["document"]["order"]["seller_pubkey"],
"buyer-visible-seller-pubkey"
);
- assert_eq!(payload["document"]["order"]["items"][0]["bin_id"], "bin-1");
+ assert_eq!(
+ payload["document"]["order"]["items"][0]["bin_id"],
+ "dozen-eggs"
+ );
assert_eq!(payload["document"]["order"]["items"][0]["bin_count"], 2);
assert_eq!(
payload["document"]["order"]["economics"]["items"][0]["quantity_amount"],
"1"
);
assert_eq!(
+ payload["document"]["order"]["economics"]["items"][0]["bin_id"],
+ "dozen-eggs"
+ );
+ assert_eq!(
payload["document"]["order"]["economics"]["pricing_basis"],
"listing_event"
);
@@ -8621,6 +8658,10 @@ mod tests {
payload["app_order"]["buyer_order_note"],
"Leave by the cooler"
);
+ assert_eq!(
+ payload["app_order"]["lines"][0]["listing_bin_id"],
+ "dozen-eggs"
+ );
cleanup_bootstrapped_runtime_paths(&paths);
}
@@ -11998,6 +12039,24 @@ mod tests {
title: &str,
created_at_ms: i64,
) {
+ append_cli_signed_buyer_listing_record_with_bin(
+ paths,
+ record_suffix,
+ listing_key,
+ title,
+ created_at_ms,
+ "bin-1",
+ );
+ }
+
+ fn append_cli_signed_buyer_listing_record_with_bin(
+ paths: &AppDesktopRuntimePaths,
+ record_suffix: &str,
+ listing_key: &str,
+ title: &str,
+ created_at_ms: i64,
+ bin_id: &str,
+ ) {
let database_path = paths
.shared_local_events_database_path()
.expect("shared local events path");
@@ -12060,8 +12119,8 @@ mod tests {
["key", listing_key],
["title", title],
["summary", "Published local eggs"],
- ["radroots:bin", "bin-1", "1", "each"],
- ["radroots:price", "bin-1", "8", "USD", "1", "each"],
+ ["radroots:bin", bin_id, "1", "each"],
+ ["radroots:price", bin_id, "8", "USD", "1", "each"],
["inventory", "9"],
["status", "active"],
["radroots:availability_start", "4102444800"],
diff --git a/crates/shared/sqlite/migrations/0015_buyer_order_listing_identity.sql b/crates/shared/sqlite/migrations/0015_buyer_order_listing_identity.sql
@@ -0,0 +1,22 @@
+ALTER TABLE products ADD COLUMN listing_bin_id TEXT;
+
+ALTER TABLE buyer_cart_lines ADD COLUMN listing_bin_id TEXT;
+ALTER TABLE buyer_cart_lines ADD COLUMN quantity_unit_label TEXT;
+ALTER TABLE buyer_cart_lines ADD COLUMN unit_price_minor_units INTEGER CHECK (
+ unit_price_minor_units IS NULL OR unit_price_minor_units >= 0
+);
+ALTER TABLE buyer_cart_lines ADD COLUMN price_currency TEXT;
+ALTER TABLE buyer_cart_lines ADD COLUMN farm_key TEXT;
+ALTER TABLE buyer_cart_lines ADD COLUMN listing_addr TEXT;
+ALTER TABLE buyer_cart_lines ADD COLUMN listing_event_id TEXT;
+ALTER TABLE buyer_cart_lines ADD COLUMN seller_pubkey TEXT;
+
+ALTER TABLE order_lines ADD COLUMN listing_bin_id TEXT;
+ALTER TABLE order_lines ADD COLUMN unit_price_minor_units INTEGER CHECK (
+ unit_price_minor_units IS NULL OR unit_price_minor_units >= 0
+);
+ALTER TABLE order_lines ADD COLUMN price_currency TEXT NOT NULL DEFAULT 'USD';
+ALTER TABLE order_lines ADD COLUMN farm_key TEXT;
+ALTER TABLE order_lines ADD COLUMN listing_addr TEXT;
+ALTER TABLE order_lines ADD COLUMN listing_event_id TEXT;
+ALTER TABLE order_lines ADD COLUMN seller_pubkey TEXT;
diff --git a/crates/shared/sqlite/src/buyer.rs b/crates/shared/sqlite/src/buyer.rs
@@ -52,6 +52,7 @@ pub struct BuyerOrderLocalEventLine {
pub quantity_display: String,
pub unit_price_minor_units: Option<u32>,
pub price_currency: String,
+ pub listing_bin_id: Option<String>,
pub farm_key: Option<String>,
pub listing_addr: Option<String>,
pub listing_event_id: Option<String>,
@@ -189,18 +190,35 @@ impl<'a> AppBuyerRepository<'a> {
})?;
for line in &cart.lines {
+ let snapshot = self.load_buyer_cart_line_snapshot(line.product_id)?;
self.connection
.execute(
"insert into buyer_cart_lines (
buyer_context_key,
product_id,
quantity,
+ listing_bin_id,
+ quantity_unit_label,
+ unit_price_minor_units,
+ price_currency,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
updated_at
- ) values (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))",
+ ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))",
params![
context_key.as_str(),
line.product_id.to_string(),
i64::from(line.quantity),
+ snapshot.listing_bin_id.as_deref(),
+ line.unit_price.unit_label.as_str(),
+ line.unit_price.amount_minor_units,
+ normalize_currency_code(&line.unit_price.currency_code),
+ snapshot.farm_key.as_deref(),
+ snapshot.listing_addr.as_deref(),
+ snapshot.listing_event_id.as_deref(),
+ snapshot.seller_pubkey.as_deref(),
],
)
.map_err(|source| AppSqliteError::Query {
@@ -404,8 +422,15 @@ impl<'a> AppBuyerRepository<'a> {
quantity_value,
quantity_unit_label,
quantity_display,
+ listing_bin_id,
+ unit_price_minor_units,
+ price_currency,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
sort_index
- ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
+ ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
params![
format!("{}:{}", order_id, line.listing.product_id),
order_id.to_string(),
@@ -413,6 +438,13 @@ impl<'a> AppBuyerRepository<'a> {
i64::from(line.quantity),
line.listing.unit_label.as_str(),
format_quantity_display(line.quantity, &line.listing.unit_label),
+ line.listing.listing_bin_id.as_deref(),
+ line.listing.price_minor_units,
+ normalize_currency_code(&line.listing.price_currency),
+ line.listing.farm_key.as_deref(),
+ line.listing.listing_addr.as_deref(),
+ line.listing.listing_event_id.as_deref(),
+ line.listing.seller_pubkey.as_deref(),
index as i64,
],
)
@@ -1132,6 +1164,45 @@ impl<'a> AppBuyerRepository<'a> {
p.unit_label,
p.price_minor_units,
p.price_currency,
+ p.listing_bin_id,
+ (
+ select li.farm_key
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ order by li.local_seq desc
+ limit 1
+ ),
+ (
+ select li.listing_addr
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ and li.listing_addr is not null
+ and trim(li.listing_addr) <> ''
+ order by li.local_seq desc
+ limit 1
+ ),
+ (
+ select li.event_id
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ and li.event_id is not null
+ and trim(li.event_id) <> ''
+ order by li.local_seq desc
+ limit 1
+ ),
+ (
+ select li.owner_pubkey
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ and li.owner_pubkey is not null
+ and trim(li.owner_pubkey) <> ''
+ order by li.local_seq desc
+ limit 1
+ ),
p.stock_count,
fw.id,
fw.label,
@@ -1174,15 +1245,20 @@ impl<'a> AppBuyerRepository<'a> {
row.get::<_, String>(7)?,
row.get::<_, Option<u32>>(8)?,
row.get::<_, String>(9)?,
- row.get::<_, Option<u32>>(10)?,
+ row.get::<_, Option<String>>(10)?,
row.get::<_, Option<String>>(11)?,
row.get::<_, Option<String>>(12)?,
row.get::<_, Option<String>>(13)?,
row.get::<_, Option<String>>(14)?,
- row.get::<_, Option<String>>(15)?,
- row.get::<_, i64>(16)?,
- row.get::<_, i64>(17)?,
- row.get::<_, i64>(18)?,
+ row.get::<_, Option<u32>>(15)?,
+ row.get::<_, Option<String>>(16)?,
+ row.get::<_, Option<String>>(17)?,
+ row.get::<_, Option<String>>(18)?,
+ row.get::<_, Option<String>>(19)?,
+ row.get::<_, Option<String>>(20)?,
+ row.get::<_, i64>(21)?,
+ row.get::<_, i64>(22)?,
+ row.get::<_, i64>(23)?,
))
})
.map_err(|source| AppSqliteError::Query {
@@ -1203,6 +1279,11 @@ impl<'a> AppBuyerRepository<'a> {
unit_label,
price_minor_units,
price_currency,
+ listing_bin_id,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
stock_count,
fulfillment_window_id,
fulfillment_window_label,
@@ -1228,6 +1309,11 @@ impl<'a> AppBuyerRepository<'a> {
unit_label,
price_minor_units,
price_currency,
+ listing_bin_id: listing_bin_id.and_then(empty_string_to_none),
+ farm_key: farm_key.and_then(empty_string_to_none),
+ listing_addr: listing_addr.and_then(empty_string_to_none),
+ listing_event_id: listing_event_id.and_then(empty_string_to_none),
+ seller_pubkey: seller_pubkey.and_then(empty_string_to_none),
stock_count,
fulfillment_window_id: parse_optional_typed_id(
"products.availability_window_id",
@@ -1265,6 +1351,22 @@ impl<'a> AppBuyerRepository<'a> {
.find(|record| record.product_id == product_id))
}
+ fn load_buyer_cart_line_snapshot(
+ &self,
+ product_id: ProductId,
+ ) -> Result<BuyerCartLineSnapshot, AppSqliteError> {
+ Ok(self
+ .load_listing_record_by_id(product_id)?
+ .map(|listing| BuyerCartLineSnapshot {
+ listing_bin_id: listing.listing_bin_id,
+ farm_key: listing.farm_key,
+ listing_addr: listing.listing_addr,
+ listing_event_id: listing.listing_event_id,
+ seller_pubkey: listing.seller_pubkey,
+ })
+ .unwrap_or_default())
+ }
+
fn load_cart_header(
&self,
context_key: &str,
@@ -1327,9 +1429,48 @@ impl<'a> AppBuyerRepository<'a> {
p.title,
p.subtitle,
p.status,
- p.unit_label,
- p.price_minor_units,
- p.price_currency,
+ coalesce(nullif(bcl.quantity_unit_label, ''), p.unit_label),
+ coalesce(bcl.unit_price_minor_units, p.price_minor_units),
+ coalesce(nullif(bcl.price_currency, ''), p.price_currency),
+ coalesce(nullif(bcl.listing_bin_id, ''), p.listing_bin_id),
+ coalesce(nullif(bcl.farm_key, ''), (
+ select li.farm_key
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ order by li.local_seq desc
+ limit 1
+ )),
+ coalesce(nullif(bcl.listing_addr, ''), (
+ select li.listing_addr
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ and li.listing_addr is not null
+ and trim(li.listing_addr) <> ''
+ order by li.local_seq desc
+ limit 1
+ )),
+ coalesce(nullif(bcl.listing_event_id, ''), (
+ select li.event_id
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ and li.event_id is not null
+ and trim(li.event_id) <> ''
+ order by li.local_seq desc
+ limit 1
+ )),
+ coalesce(nullif(bcl.seller_pubkey, ''), (
+ select li.owner_pubkey
+ from local_interop_imports li
+ where li.projected_kind = 'listing'
+ and li.projected_id = p.id
+ and li.owner_pubkey is not null
+ and trim(li.owner_pubkey) <> ''
+ order by li.local_seq desc
+ limit 1
+ )),
p.stock_count,
fw.id,
fw.label,
@@ -1376,15 +1517,20 @@ impl<'a> AppBuyerRepository<'a> {
row.get::<_, String>(8)?,
row.get::<_, Option<u32>>(9)?,
row.get::<_, String>(10)?,
- row.get::<_, Option<u32>>(11)?,
+ row.get::<_, Option<String>>(11)?,
row.get::<_, Option<String>>(12)?,
row.get::<_, Option<String>>(13)?,
row.get::<_, Option<String>>(14)?,
row.get::<_, Option<String>>(15)?,
- row.get::<_, Option<String>>(16)?,
- row.get::<_, i64>(17)?,
- row.get::<_, i64>(18)?,
- row.get::<_, i64>(19)?,
+ row.get::<_, Option<u32>>(16)?,
+ row.get::<_, Option<String>>(17)?,
+ row.get::<_, Option<String>>(18)?,
+ row.get::<_, Option<String>>(19)?,
+ row.get::<_, Option<String>>(20)?,
+ row.get::<_, Option<String>>(21)?,
+ row.get::<_, i64>(22)?,
+ row.get::<_, i64>(23)?,
+ row.get::<_, i64>(24)?,
))
})
.map_err(|source| AppSqliteError::Query {
@@ -1406,6 +1552,11 @@ impl<'a> AppBuyerRepository<'a> {
unit_label,
price_minor_units,
price_currency,
+ listing_bin_id,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
stock_count,
fulfillment_window_id,
fulfillment_window_label,
@@ -1430,6 +1581,11 @@ impl<'a> AppBuyerRepository<'a> {
unit_label,
price_minor_units,
price_currency,
+ listing_bin_id: listing_bin_id.and_then(empty_string_to_none),
+ farm_key: farm_key.and_then(empty_string_to_none),
+ listing_addr: listing_addr.and_then(empty_string_to_none),
+ listing_event_id: listing_event_id.and_then(empty_string_to_none),
+ seller_pubkey: seller_pubkey.and_then(empty_string_to_none),
stock_count,
fulfillment_window_id: parse_optional_typed_id(
"products.availability_window_id",
@@ -1510,48 +1666,14 @@ impl<'a> AppBuyerRepository<'a> {
ol.quantity_value,
ol.quantity_unit_label,
ol.quantity_display,
- p.price_minor_units,
- p.price_currency,
- (
- select li.farm_key
- from local_interop_imports li
- where li.projected_kind = 'listing'
- and li.projected_id = p.id
- order by li.local_seq desc
- limit 1
- ),
- (
- select li.listing_addr
- from local_interop_imports li
- where li.projected_kind = 'listing'
- and li.projected_id = p.id
- and li.listing_addr is not null
- and trim(li.listing_addr) <> ''
- order by li.local_seq desc
- limit 1
- ),
- (
- select li.event_id
- from local_interop_imports li
- where li.projected_kind = 'listing'
- and li.projected_id = p.id
- and li.event_id is not null
- and trim(li.event_id) <> ''
- order by li.local_seq desc
- limit 1
- ),
- (
- select li.owner_pubkey
- from local_interop_imports li
- where li.projected_kind = 'listing'
- and li.projected_id = p.id
- and li.owner_pubkey is not null
- and trim(li.owner_pubkey) <> ''
- order by li.local_seq desc
- limit 1
- )
+ ol.unit_price_minor_units,
+ ol.price_currency,
+ ol.listing_bin_id,
+ ol.farm_key,
+ ol.listing_addr,
+ ol.listing_event_id,
+ ol.seller_pubkey
from order_lines ol
- left join products p on p.id = substr(ol.id, length(?1) + 2)
where ol.order_id = ?1
order by ol.sort_index asc, ol.id asc",
)
@@ -1573,6 +1695,7 @@ impl<'a> AppBuyerRepository<'a> {
row.get::<_, Option<String>>(8)?,
row.get::<_, Option<String>>(9)?,
row.get::<_, Option<String>>(10)?,
+ row.get::<_, Option<String>>(11)?,
))
})
.map_err(|source| AppSqliteError::Query {
@@ -1590,6 +1713,7 @@ impl<'a> AppBuyerRepository<'a> {
quantity_display,
unit_price_minor_units,
price_currency,
+ listing_bin_id,
farm_key,
listing_addr,
listing_event_id,
@@ -1617,6 +1741,7 @@ impl<'a> AppBuyerRepository<'a> {
quantity_display,
unit_price_minor_units,
price_currency: price_currency.unwrap_or_else(|| "USD".to_owned()),
+ listing_bin_id: listing_bin_id.and_then(empty_string_to_none),
farm_key: farm_key.and_then(empty_string_to_none),
listing_addr: listing_addr.and_then(empty_string_to_none),
listing_event_id: listing_event_id.and_then(empty_string_to_none),
@@ -1861,6 +1986,11 @@ struct BuyerListingRecord {
unit_label: String,
price_minor_units: Option<u32>,
price_currency: String,
+ listing_bin_id: Option<String>,
+ farm_key: Option<String>,
+ listing_addr: Option<String>,
+ listing_event_id: Option<String>,
+ seller_pubkey: Option<String>,
stock_count: Option<u32>,
fulfillment_window_id: Option<FulfillmentWindowId>,
fulfillment_window_label: Option<String>,
@@ -2032,6 +2162,15 @@ struct BuyerCartLineRecord {
quantity: u32,
}
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct BuyerCartLineSnapshot {
+ listing_bin_id: Option<String>,
+ farm_key: Option<String>,
+ listing_addr: Option<String>,
+ listing_event_id: Option<String>,
+ seller_pubkey: Option<String>,
+}
+
impl BuyerCartLineRecord {
fn into_projection(self) -> Result<BuyerCartLineProjection, AppSqliteError> {
let unit_price =
diff --git a/crates/shared/sqlite/src/lib.rs b/crates/shared/sqlite/src/lib.rs
@@ -798,9 +798,39 @@ mod tests {
"quantity_unit_label"
));
assert!(column_exists(connection, "order_lines", "quantity_display"));
+ assert!(column_exists(connection, "order_lines", "listing_bin_id"));
+ assert!(column_exists(
+ connection,
+ "order_lines",
+ "unit_price_minor_units"
+ ));
+ assert!(column_exists(connection, "order_lines", "price_currency"));
+ assert!(column_exists(connection, "order_lines", "listing_addr"));
+ assert!(column_exists(connection, "products", "listing_bin_id"));
assert!(column_exists(connection, "buyer_carts", "buyer_email"));
assert!(column_exists(connection, "buyer_carts", "buyer_phone"));
assert!(column_exists(connection, "buyer_carts", "buyer_order_note"));
+ assert!(column_exists(
+ connection,
+ "buyer_cart_lines",
+ "listing_bin_id"
+ ));
+ assert!(column_exists(
+ connection,
+ "buyer_cart_lines",
+ "quantity_unit_label"
+ ));
+ assert!(column_exists(
+ connection,
+ "buyer_cart_lines",
+ "unit_price_minor_units"
+ ));
+ assert!(column_exists(connection, "buyer_cart_lines", "farm_key"));
+ assert!(column_exists(
+ connection,
+ "buyer_cart_lines",
+ "listing_event_id"
+ ));
assert!(column_exists(connection, "orders", "buyer_context_key"));
assert!(column_exists(connection, "orders", "buyer_email"));
assert!(column_exists(connection, "orders", "buyer_phone"));
diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs
@@ -372,6 +372,7 @@ impl<'a> AppLocalInteropRepository<'a> {
let unit_label = string_at(document, &["primary_bin", "quantity_unit"])
.or_else(|| string_at(document, &["primary_bin", "price_per_unit"]))
.unwrap_or_default();
+ let listing_bin_id = string_at(document, &["primary_bin", "bin_id"]);
let price_minor_units = string_at(document, &["primary_bin", "price_amount"])
.and_then(|price| parse_decimal_minor_units(price.as_str()));
let price_currency = string_at(document, &["primary_bin", "price_currency"])
@@ -389,6 +390,7 @@ impl<'a> AppLocalInteropRepository<'a> {
price_currency,
stock_count,
availability_window_id: None,
+ listing_bin_id,
})?;
Ok(Some(ProjectionRecord {
kind: "listing",
@@ -506,6 +508,9 @@ impl<'a> AppLocalInteropRepository<'a> {
.or_else(|| tag_index_value(tags, "summary", 1))
.unwrap_or_default();
let bin = content.as_ref().and_then(primary_bin);
+ let listing_bin_id = bin
+ .and_then(|value| string_at(value, &["bin_id"]))
+ .or_else(|| tag_index_value(tags, "radroots:bin", 1));
let unit_label = bin
.and_then(|value| {
string_at(value, &["quantity", "unit"])
@@ -572,6 +577,7 @@ impl<'a> AppLocalInteropRepository<'a> {
price_currency,
stock_count,
availability_window_id,
+ listing_bin_id,
})?;
Ok(Some(ProjectionRecord {
kind: "listing",
@@ -852,8 +858,9 @@ impl<'a> AppLocalInteropRepository<'a> {
price_currency,
stock_count,
availability_window_id,
+ listing_bin_id,
updated_at
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
ON CONFLICT(id) DO UPDATE SET
farm_id = excluded.farm_id,
title = excluded.title,
@@ -874,6 +881,7 @@ impl<'a> AppLocalInteropRepository<'a> {
THEN products.availability_window_id
ELSE excluded.availability_window_id
END,
+ listing_bin_id = coalesce(excluded.listing_bin_id, products.listing_bin_id),
updated_at = excluded.updated_at",
params![
projection.product_id.to_string(),
@@ -886,6 +894,7 @@ impl<'a> AppLocalInteropRepository<'a> {
projection.price_currency.as_str(),
projection.stock_count,
projection.availability_window_id.map(|id| id.to_string()),
+ projection.listing_bin_id.as_deref(),
],
)
.map_err(|source| AppSqliteError::Query {
@@ -1071,6 +1080,7 @@ struct ProductProjection {
price_currency: String,
stock_count: Option<u32>,
availability_window_id: Option<FulfillmentWindowId>,
+ listing_bin_id: Option<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
diff --git a/crates/shared/sqlite/src/migrations.rs b/crates/shared/sqlite/src/migrations.rs
@@ -60,6 +60,10 @@ const MIGRATIONS: &[Migration] = &[
version: 14,
sql: include_str!("../migrations/0014_buyer_order_coordination.sql"),
},
+ Migration {
+ version: 15,
+ sql: include_str!("../migrations/0015_buyer_order_listing_identity.sql"),
+ },
];
pub fn latest_schema_version() -> u32 {