app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Mcrates/launchers/desktop/src/runtime.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Acrates/shared/sqlite/migrations/0015_buyer_order_listing_identity.sql | 22++++++++++++++++++++++
Mcrates/shared/sqlite/src/buyer.rs | 251+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mcrates/shared/sqlite/src/lib.rs | 30++++++++++++++++++++++++++++++
Mcrates/shared/sqlite/src/local_interop.rs | 12+++++++++++-
Mcrates/shared/sqlite/src/migrations.rs | 4++++
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 {