commit e3c9a2a891df00357784ac495786394ef508df9f
parent 759c5e6e80fe77106d071945cf3d003786378b70
Author: triesap <tyson@radroots.org>
Date: Sat, 23 May 2026 21:12:47 +0000
sqlite: project interop listings for buyers
Diffstat:
1 file changed, 746 insertions(+), 2 deletions(-)
diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs
@@ -2,7 +2,7 @@ use std::{fs, path::Path};
use radroots_app_models::{
FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary,
- ProductId, ProductStatus,
+ FulfillmentWindowId, PickupLocationId, ProductId, ProductStatus,
};
use radroots_local_events::{
LocalEventRecord, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus,
@@ -388,6 +388,7 @@ impl<'a> AppLocalInteropRepository<'a> {
price_minor_units,
price_currency,
stock_count,
+ availability_window_id: None,
})?;
Ok(Some(ProjectionRecord {
kind: "listing",
@@ -540,6 +541,26 @@ impl<'a> AppLocalInteropRepository<'a> {
let Some(status) = signed_listing_product_status(record, content.as_ref(), tags) else {
return Ok(None);
};
+ let fulfillment_method = signed_listing_fulfillment_method(content.as_ref(), tags);
+ let availability_window_id = if status == ProductStatus::Published {
+ match fulfillment_method {
+ Some(method) => self.ensure_signed_listing_availability_window(
+ farm_id,
+ listing_key.as_str(),
+ content.as_ref(),
+ tags,
+ method,
+ )?,
+ None => None,
+ }
+ } else {
+ None
+ };
+ if availability_window_id.is_some()
+ && let Some(method) = fulfillment_method
+ {
+ self.mark_farm_buyer_visible(farm_id, record, method)?;
+ }
self.upsert_product(ProductProjection {
product_id,
farm_id,
@@ -550,6 +571,7 @@ impl<'a> AppLocalInteropRepository<'a> {
price_minor_units,
price_currency,
stock_count,
+ availability_window_id,
})?;
Ok(Some(ProjectionRecord {
kind: "listing",
@@ -579,6 +601,77 @@ impl<'a> AppLocalInteropRepository<'a> {
Ok(())
}
+ fn mark_farm_buyer_visible(
+ &self,
+ farm_id: FarmId,
+ record: &LocalEventRecord,
+ method: FarmOrderMethod,
+ ) -> Result<(), AppSqliteError> {
+ self.connection
+ .execute(
+ "UPDATE farms
+ SET readiness = 'ready',
+ updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
+ WHERE id = ?1",
+ [farm_id.to_string()],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "mark local interop farm buyer visible",
+ source,
+ })?;
+ let Some(account_id) = record
+ .owner_account_id
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ else {
+ return Ok(());
+ };
+ let display_name = self
+ .load_farm_display_name(farm_id)?
+ .unwrap_or_else(|| "Local farm".to_owned());
+ self.connection
+ .execute(
+ "INSERT INTO account_farm_setups (
+ account_id,
+ farm_name,
+ location_or_service_area,
+ pickup_enabled,
+ delivery_enabled,
+ shipping_enabled,
+ saved_farm_id,
+ saved_farm_display_name,
+ saved_farm_readiness,
+ updated_at
+ ) VALUES (?1, ?2, '', ?3, ?4, ?5, ?6, ?2, 'ready', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+ ON CONFLICT(account_id) DO UPDATE SET
+ farm_name = CASE
+ WHEN trim(account_farm_setups.farm_name) = '' THEN excluded.farm_name
+ ELSE account_farm_setups.farm_name
+ END,
+ pickup_enabled = max(account_farm_setups.pickup_enabled, excluded.pickup_enabled),
+ delivery_enabled = max(account_farm_setups.delivery_enabled, excluded.delivery_enabled),
+ shipping_enabled = max(account_farm_setups.shipping_enabled, excluded.shipping_enabled),
+ saved_farm_id = excluded.saved_farm_id,
+ saved_farm_display_name = excluded.saved_farm_display_name,
+ saved_farm_readiness = excluded.saved_farm_readiness,
+ updated_at = excluded.updated_at",
+ params![
+ account_id,
+ display_name.as_str(),
+ i64::from(method == FarmOrderMethod::Pickup),
+ i64::from(method == FarmOrderMethod::Delivery),
+ i64::from(method == FarmOrderMethod::Shipping),
+ farm_id.to_string(),
+ ],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "upsert local interop buyer fulfillment method",
+ source,
+ })?;
+ Ok(())
+ }
+
fn ensure_farm_exists(&self, farm_id: FarmId) -> Result<(), AppSqliteError> {
let exists = self
.connection
@@ -601,6 +694,150 @@ impl<'a> AppLocalInteropRepository<'a> {
Ok(())
}
+ fn load_farm_display_name(&self, farm_id: FarmId) -> Result<Option<String>, AppSqliteError> {
+ self.connection
+ .query_row(
+ "SELECT display_name FROM farms WHERE id = ?1 LIMIT 1",
+ [farm_id.to_string()],
+ |row| row.get::<_, String>(0),
+ )
+ .optional()
+ .map_err(|source| AppSqliteError::Query {
+ operation: "load local interop farm display name",
+ source,
+ })
+ }
+
+ fn ensure_signed_listing_availability_window(
+ &self,
+ farm_id: FarmId,
+ listing_key: &str,
+ content: Option<&Value>,
+ tags: Option<&Value>,
+ method: FarmOrderMethod,
+ ) -> Result<Option<FulfillmentWindowId>, AppSqliteError> {
+ let Some(window) = signed_listing_availability_window(content, tags) else {
+ return Ok(None);
+ };
+ let starts_at =
+ self.unix_epoch_to_utc_timestamp(window.start, "format listing availability start")?;
+ let ends_at =
+ self.unix_epoch_to_utc_timestamp(window.end, "format listing availability end")?;
+ if ends_at <= starts_at {
+ return Ok(None);
+ }
+ let pickup_location_id = if method == FarmOrderMethod::Pickup {
+ let Some(location_primary) = signed_listing_location_primary(content, tags) else {
+ return Ok(None);
+ };
+ Some(self.upsert_signed_listing_pickup_location(farm_id, location_primary.as_str())?)
+ } else {
+ None
+ };
+ let farm_id_string = farm_id.to_string();
+ let fulfillment_window_id = FulfillmentWindowId::from(deterministic_uuid(
+ "radroots-app-local-interop-fulfillment-window",
+ Some(farm_id_string.as_str()),
+ listing_key,
+ ));
+ self.connection
+ .execute(
+ "INSERT INTO fulfillment_windows (
+ id,
+ farm_id,
+ starts_at,
+ ends_at,
+ capacity_limit,
+ created_at,
+ updated_at,
+ pickup_location_id,
+ label,
+ order_cutoff_at
+ ) VALUES (?1, ?2, ?3, ?4, null, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), ?5, '', ?3)
+ ON CONFLICT(id) DO UPDATE SET
+ farm_id = excluded.farm_id,
+ starts_at = excluded.starts_at,
+ ends_at = excluded.ends_at,
+ pickup_location_id = excluded.pickup_location_id,
+ order_cutoff_at = excluded.order_cutoff_at,
+ updated_at = excluded.updated_at",
+ params![
+ fulfillment_window_id.to_string(),
+ farm_id_string.as_str(),
+ starts_at.as_str(),
+ ends_at.as_str(),
+ pickup_location_id.map(|id| id.to_string()),
+ ],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "upsert local interop listing fulfillment window",
+ source,
+ })?;
+ Ok(Some(fulfillment_window_id))
+ }
+
+ fn upsert_signed_listing_pickup_location(
+ &self,
+ farm_id: FarmId,
+ location_primary: &str,
+ ) -> Result<PickupLocationId, AppSqliteError> {
+ let farm_id_string = farm_id.to_string();
+ let pickup_location_id = PickupLocationId::from(deterministic_uuid(
+ "radroots-app-local-interop-pickup-location",
+ Some(farm_id_string.as_str()),
+ location_primary,
+ ));
+ self.connection
+ .execute(
+ "INSERT INTO pickup_locations (
+ id,
+ farm_id,
+ label,
+ address_line,
+ directions,
+ is_default,
+ created_at,
+ updated_at
+ ) VALUES (?1, ?2, ?3, ?3, null, 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+ ON CONFLICT(id) DO UPDATE SET
+ farm_id = excluded.farm_id,
+ label = excluded.label,
+ address_line = excluded.address_line,
+ updated_at = excluded.updated_at",
+ params![
+ pickup_location_id.to_string(),
+ farm_id_string.as_str(),
+ location_primary,
+ ],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "upsert local interop listing pickup location",
+ source,
+ })?;
+ Ok(pickup_location_id)
+ }
+
+ fn unix_epoch_to_utc_timestamp(
+ &self,
+ seconds: u64,
+ operation: &'static str,
+ ) -> Result<String, AppSqliteError> {
+ let seconds = i64::try_from(seconds).map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "listing availability timestamp is out of range",
+ })?;
+ let timestamp = self
+ .connection
+ .query_row(
+ "SELECT strftime('%Y-%m-%dT%H:%M:%SZ', ?1, 'unixepoch')",
+ [seconds],
+ |row| row.get::<_, Option<String>>(0),
+ )
+ .map_err(|source| AppSqliteError::Query { operation, source })?;
+ timestamp.ok_or(AppSqliteError::InvalidProjection {
+ reason: "listing availability timestamp is invalid",
+ })
+ }
+
fn upsert_product(&self, projection: ProductProjection) -> Result<(), AppSqliteError> {
self.connection
.execute(
@@ -616,7 +853,7 @@ impl<'a> AppLocalInteropRepository<'a> {
stock_count,
availability_window_id,
updated_at
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
ON CONFLICT(id) DO UPDATE SET
farm_id = excluded.farm_id,
title = excluded.title,
@@ -631,6 +868,12 @@ impl<'a> AppLocalInteropRepository<'a> {
price_minor_units = excluded.price_minor_units,
price_currency = excluded.price_currency,
stock_count = excluded.stock_count,
+ availability_window_id = CASE
+ WHEN excluded.status = 'draft'
+ AND products.status IN ('published', 'paused', 'archived')
+ THEN products.availability_window_id
+ ELSE excluded.availability_window_id
+ END,
updated_at = excluded.updated_at",
params![
projection.product_id.to_string(),
@@ -642,6 +885,7 @@ impl<'a> AppLocalInteropRepository<'a> {
projection.price_minor_units,
projection.price_currency.as_str(),
projection.stock_count,
+ projection.availability_window_id.map(|id| id.to_string()),
],
)
.map_err(|source| AppSqliteError::Query {
@@ -826,6 +1070,7 @@ struct ProductProjection {
price_minor_units: Option<u32>,
price_currency: String,
stock_count: Option<u32>,
+ availability_window_id: Option<FulfillmentWindowId>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -974,6 +1219,15 @@ fn parse_u32_quantity(value: &str) -> Option<u32> {
whole.parse::<u32>().ok()
}
+fn parse_u64_quantity(value: &str) -> Option<u64> {
+ let value = value.trim();
+ if value.is_empty() || value.starts_with('-') {
+ return None;
+ }
+ let whole = value.split_once('.').map_or(value, |(whole, _)| whole);
+ whole.parse::<u64>().ok()
+}
+
fn signed_listing_product_status(
record: &LocalEventRecord,
content: Option<&Value>,
@@ -993,6 +1247,58 @@ fn signed_listing_product_status(
}
}
+fn signed_listing_fulfillment_method(
+ content: Option<&Value>,
+ tags: Option<&Value>,
+) -> Option<FarmOrderMethod> {
+ content.and_then(delivery_method_from_content).or_else(|| {
+ tag_index_value(tags, "delivery", 1).and_then(|method| farm_order_method(&method))
+ })
+}
+
+fn delivery_method_from_content(content: &Value) -> Option<FarmOrderMethod> {
+ string_at(content, &["delivery_method", "kind"])
+ .or_else(|| string_at(content, &["delivery", "method"]))
+ .or_else(|| string_at(content, &["delivery_method"]))
+ .and_then(|method| farm_order_method(method.as_str()))
+}
+
+fn signed_listing_availability_window(
+ content: Option<&Value>,
+ tags: Option<&Value>,
+) -> Option<ListingAvailabilityWindow> {
+ let start = content
+ .and_then(|content| string_at(content, &["availability", "amount", "start"]))
+ .or_else(|| content.and_then(|content| string_at(content, &["availability", "start"])))
+ .or_else(|| tag_index_value(tags, "radroots:availability_start", 1))
+ .and_then(|value| parse_u64_quantity(value.as_str()));
+ let end = content
+ .and_then(|content| string_at(content, &["availability", "amount", "end"]))
+ .or_else(|| content.and_then(|content| string_at(content, &["availability", "end"])))
+ .or_else(|| tag_index_value(tags, "expires_at", 1))
+ .and_then(|value| parse_u64_quantity(value.as_str()));
+
+ match (start, end) {
+ (Some(start), Some(end)) if end > start => Some(ListingAvailabilityWindow { start, end }),
+ _ => None,
+ }
+}
+
+fn signed_listing_location_primary(
+ content: Option<&Value>,
+ tags: Option<&Value>,
+) -> Option<String> {
+ content
+ .and_then(|content| string_at(content, &["location", "primary"]))
+ .or_else(|| tag_index_value(tags, "location", 1))
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct ListingAvailabilityWindow {
+ start: u64,
+ end: u64,
+}
+
fn signed_listing_lifecycle(
content: Option<&Value>,
tags: Option<&Value>,
@@ -1108,6 +1414,9 @@ fn farm_readiness_storage_key(readiness: FarmReadiness) -> &'static str {
#[cfg(test)]
mod tests {
+ use std::collections::BTreeSet;
+
+ use radroots_app_models::{FarmOrderMethod, ProductAvailabilityState};
use radroots_local_events::{
LocalEventRecordInput, LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily,
LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
@@ -1234,6 +1543,117 @@ mod tests {
}
}
+ fn signed_market_listing_record(
+ record_id: &str,
+ owner_pubkey: &str,
+ farm_key: &str,
+ listing_key: &str,
+ title: &str,
+ inventory_available: &str,
+ status_tag: &str,
+ delivery_method: &str,
+ location_primary: &str,
+ availability_start: u64,
+ availability_end: u64,
+ record_status: LocalRecordStatus,
+ outbox_status: PublishOutboxStatus,
+ ) -> LocalEventRecordInput {
+ let relay_delivery_json = match outbox_status {
+ PublishOutboxStatus::Acknowledged => Some(json!({
+ "state": "acknowledged",
+ "acknowledged_relays": ["ws://127.0.0.1:1234/"]
+ })),
+ PublishOutboxStatus::Failed => Some(json!({
+ "state": "failed",
+ "failed_relays": ["ws://127.0.0.1:1234/"]
+ })),
+ PublishOutboxStatus::Pending | PublishOutboxStatus::None => None,
+ };
+ let content = json!({
+ "d_tag": listing_key,
+ "status": status_tag,
+ "farm": {
+ "pubkey": owner_pubkey,
+ "d_tag": farm_key,
+ },
+ "product": {
+ "key": listing_key,
+ "title": title,
+ "summary": "Published local listing",
+ },
+ "availability": {
+ "kind": "window",
+ "amount": {
+ "start": availability_start,
+ "end": availability_end,
+ },
+ },
+ "delivery_method": {
+ "kind": delivery_method,
+ },
+ "location": {
+ "primary": location_primary,
+ },
+ });
+
+ LocalEventRecordInput {
+ record_id: record_id.to_owned(),
+ family: LocalRecordFamily::SignedEvent,
+ status: record_status,
+ source_runtime: SourceRuntime::Cli,
+ created_at_ms: 1100,
+ inserted_at_ms: 1101,
+ owner_account_id: Some("seller-account".to_owned()),
+ owner_pubkey: Some(owner_pubkey.to_owned()),
+ farm_id: Some(farm_key.to_owned()),
+ listing_addr: Some(format!("30402:{owner_pubkey}:{listing_key}")),
+ local_work_json: None,
+ event_id: Some(format!("event-{record_id}")),
+ event_kind: Some(KIND_LISTING),
+ event_pubkey: Some(owner_pubkey.to_owned()),
+ event_created_at: Some(1100),
+ event_tags_json: Some(json!([
+ ["d", listing_key],
+ ["a", format!("30340:{owner_pubkey}:{farm_key}")],
+ ["key", listing_key],
+ ["title", title],
+ ["summary", "Published local listing"],
+ ["radroots:bin", "bin-1", "1", "each"],
+ ["radroots:price", "bin-1", "8", "USD", "1", "each"],
+ ["inventory", inventory_available],
+ ["status", status_tag],
+ [
+ "radroots:availability_start",
+ availability_start.to_string()
+ ],
+ ["expires_at", availability_end.to_string()],
+ ["delivery", delivery_method],
+ ["location", location_primary],
+ ])),
+ event_content: Some(content.to_string()),
+ event_sig: Some("signature".to_owned()),
+ raw_event_json: Some(json!({
+ "id": format!("event-{record_id}"),
+ "kind": KIND_LISTING,
+ "pubkey": owner_pubkey,
+ "content": content.to_string(),
+ })),
+ outbox_status,
+ relay_set_fingerprint: Some("relay-set".to_owned()),
+ relay_delivery_json,
+ }
+ }
+
+ fn buyer_listing_titles(app_store: &AppSqliteStore) -> Vec<String> {
+ app_store
+ .load_buyer_listings("", &BTreeSet::new())
+ .expect("buyer listings should load")
+ .rows
+ .into_iter()
+ .map(|row| row.title)
+ .collect()
+ }
+
fn app_d_tag_from_uuid(uuid: Uuid) -> String {
const ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -1661,6 +2081,330 @@ mod tests {
}
#[test]
+ fn cli_origin_signed_window_listing_projects_into_buyer_browse_and_search() {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ let farm_key = "AAAAAAAAAAAAAAAAAAAAAA";
+ let listing_key = "BBBBBBBBBBBBBBBBBBBBBB";
+ events
+ .append_record(&signed_market_listing_record(
+ "buyer-visible-cli",
+ "seller-pubkey",
+ farm_key,
+ listing_key,
+ "Buyer Visible Eggs",
+ "9",
+ "active",
+ "pickup",
+ "North barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ))
+ .expect("append signed listing");
+
+ let report = app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import signed listing");
+ let browse = app_store
+ .load_buyer_listings("", &BTreeSet::new())
+ .expect("buyer browse should load");
+ let search = app_store
+ .load_buyer_listings("eggs", &BTreeSet::from([FarmOrderMethod::Pickup]))
+ .expect("buyer search should load");
+ let detail = app_store
+ .load_buyer_product_detail(search.rows[0].product_id)
+ .expect("buyer detail should load")
+ .expect("buyer detail should exist");
+
+ assert_eq!(report.imported_records, 1);
+ assert_eq!(browse.rows.len(), 1);
+ assert_eq!(search.rows.len(), 1);
+ assert_eq!(search.rows[0].title, "Buyer Visible Eggs");
+ assert_eq!(
+ search.rows[0].availability.state,
+ ProductAvailabilityState::Scheduled
+ );
+ assert_eq!(search.rows[0].stock.quantity, Some(9));
+ assert_eq!(
+ search.rows[0].fulfillment_methods,
+ BTreeSet::from([FarmOrderMethod::Pickup])
+ );
+ assert_eq!(detail.listing.title, "Buyer Visible Eggs");
+ }
+
+ #[test]
+ fn app_origin_signed_window_listing_converges_into_buyer_visibility() {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ let farm_uuid = Uuid::from_u128(0x55555555555545558555555555555555);
+ let product_uuid = Uuid::from_u128(0x66666666666646668666666666666666);
+ let farm_key = app_d_tag_from_uuid(farm_uuid);
+ let listing_key = app_d_tag_from_uuid(product_uuid);
+ let listing_addr = format!("30402:app-seller-pubkey:{listing_key}");
+ let app_farm_record = app_local_work_record(
+ "app:local_work:farm:buyer-visible",
+ farm_key.as_str(),
+ json!({
+ "record_kind": "farm_config_v1",
+ "document": {
+ "selection": {
+ "account": "seller-account",
+ "farm_d_tag": farm_key
+ },
+ "profile": {
+ "display_name": "App Farm"
+ },
+ "farm": {
+ "d_tag": farm_key,
+ "name": "App Farm",
+ "location": {
+ "primary": "app farmstand"
+ }
+ }
+ }
+ }),
+ );
+ let mut app_listing_record = app_local_work_record(
+ "app:local_work:listing:buyer-visible",
+ farm_key.as_str(),
+ json!({
+ "record_kind": "listing_draft_v1",
+ "document": {
+ "listing": {
+ "d_tag": listing_key,
+ "farm_d_tag": farm_key
+ },
+ "seller_actor": {
+ "account_id": "seller-account",
+ "pubkey": "app-seller-pubkey"
+ },
+ "product": {
+ "key": listing_key,
+ "title": "App Draft Eggs",
+ "summary": "Fresh app-origin eggs"
+ },
+ "primary_bin": {
+ "quantity_unit": "each",
+ "price_amount": "7",
+ "price_currency": "USD"
+ },
+ "inventory": {
+ "available": "12"
+ }
+ }
+ }),
+ );
+ app_listing_record.listing_addr = Some(listing_addr);
+ events
+ .append_record(&app_farm_record)
+ .expect("append app farm local work");
+ events
+ .append_record(&app_listing_record)
+ .expect("append app listing local work");
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import app local records");
+ events
+ .append_record(&signed_market_listing_record(
+ "buyer-visible-app-origin",
+ "app-seller-pubkey",
+ farm_key.as_str(),
+ listing_key.as_str(),
+ "Buyer Visible App Eggs",
+ "11",
+ "active",
+ "pickup",
+ "App farmstand pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ))
+ .expect("append signed app-origin listing");
+
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import signed app-origin listing");
+ let buyer_listings = app_store
+ .load_buyer_listings("app eggs", &BTreeSet::new())
+ .expect("buyer listings should load");
+
+ assert_eq!(buyer_listings.rows.len(), 1);
+ assert_eq!(buyer_listings.rows[0].product_id.as_uuid(), product_uuid);
+ assert_eq!(buyer_listings.rows[0].title, "Buyer Visible App Eggs");
+ assert_eq!(buyer_listings.rows[0].stock.quantity, Some(11));
+ }
+
+ #[test]
+ fn buyer_visibility_rejects_incomplete_unpublished_stale_and_unsupported_records() {
+ for record in [
+ signed_market_listing_record(
+ "pending-window",
+ "seller-pubkey",
+ "AAAAAAAAAAAAAAAAAAAAAA",
+ "BBBBBBBBBBBBBBBBBBBBBB",
+ "Pending Eggs",
+ "8",
+ "active",
+ "pickup",
+ "Pending barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::PendingPublish,
+ PublishOutboxStatus::Pending,
+ ),
+ signed_market_listing_record(
+ "sold-out-window",
+ "seller-pubkey",
+ "CCCCCCCCCCCCCCCCCCCCCC",
+ "DDDDDDDDDDDDDDDDDDDDDD",
+ "Sold Out Eggs",
+ "0",
+ "active",
+ "pickup",
+ "South barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ),
+ signed_market_listing_record(
+ "expired-window",
+ "seller-pubkey",
+ "EEEEEEEEEEEEEEEEEEEEEE",
+ "FFFFFFFFFFFFFFFFFFFFFF",
+ "Expired Eggs",
+ "8",
+ "active",
+ "pickup",
+ "East barn pickup",
+ 946_684_800,
+ 946_771_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ),
+ signed_market_listing_record(
+ "unsupported-fulfillment",
+ "seller-pubkey",
+ "GGGGGGGGGGGGGGGGGGGGGG",
+ "HHHHHHHHHHHHHHHHHHHHHH",
+ "Unsupported Eggs",
+ "8",
+ "active",
+ "other",
+ "Unknown exchange point",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ),
+ signed_listing_record(
+ "status-only",
+ "IIIIIIIIIIIIIIIIIIIIII",
+ "JJJJJJJJJJJJJJJJJJJJJJ",
+ "active",
+ ),
+ ] {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ events.append_record(&record).expect("append record");
+
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import hidden listing record");
+
+ assert!(buyer_listing_titles(&app_store).is_empty());
+ }
+
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ let farm_key = "KKKKKKKKKKKKKKKKKKKKKK";
+ let listing_key = "LLLLLLLLLLLLLLLLLLLLLL";
+ events
+ .append_record(&local_work_record(
+ "local-only-listing",
+ farm_key,
+ json!({
+ "record_kind": "listing_draft_v1",
+ "document": {
+ "listing": {
+ "d_tag": listing_key,
+ "farm_d_tag": farm_key
+ },
+ "product": {
+ "title": "Local Only Eggs"
+ },
+ "primary_bin": {
+ "quantity_unit": "each",
+ "price_amount": "7",
+ "price_currency": "USD"
+ },
+ "inventory": {
+ "available": "7"
+ }
+ }
+ }),
+ ))
+ .expect("append local-only listing");
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import local-only listing");
+ assert!(buyer_listing_titles(&app_store).is_empty());
+
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ events
+ .append_record(&signed_market_listing_record(
+ "current-active-window",
+ "seller-pubkey",
+ farm_key,
+ listing_key,
+ "Current Eggs",
+ "8",
+ "active",
+ "pickup",
+ "West barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ))
+ .expect("append active listing");
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import active listing");
+ assert_eq!(buyer_listing_titles(&app_store), vec!["Current Eggs"]);
+ events
+ .append_record(&signed_market_listing_record(
+ "newer-archived-window",
+ "seller-pubkey",
+ farm_key,
+ listing_key,
+ "Archived Eggs",
+ "8",
+ "archived",
+ "pickup",
+ "West barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ))
+ .expect("append archived listing");
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import archived listing");
+ assert!(buyer_listing_titles(&app_store).is_empty());
+ }
+
+ #[test]
fn signed_farm_import_prefers_event_identity_over_local_owner_metadata() {
let app_store =
AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");