commit 2fa1bd6d4874c7954832938c3c23fcd0548ae359
parent 6053d1e30b60c8da6bb644e73e16da9d8908f1f6
Author: triesap <tyson@radroots.org>
Date: Mon, 25 May 2026 19:42:30 +0000
sqlite: project signed order events
- decode supported signed order request and decision events during local interop import
- project relay order requests into seller and app-account buyer order state
- apply accepted seller decisions to projected app order status
- keep malformed order events as signed-event evidence with focused sqlite tests
Diffstat:
4 files changed, 860 insertions(+), 7 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -5140,6 +5140,9 @@ version = "0.1.0"
dependencies = [
"radroots_app_models",
"radroots_app_sync",
+ "radroots_core",
+ "radroots_events",
+ "radroots_events_codec",
"radroots_local_events",
"radroots_sql_core",
"rusqlite",
diff --git a/Cargo.toml b/Cargo.toml
@@ -30,6 +30,8 @@ gpui-component = "0.5.1"
gpui-component-assets = "0.5.1"
mf2_i18n = { git = "https://github.com/triesap/mf2_i18n.git", rev = "e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" }
radroots_core = { path = "../lib/crates/core", default-features = false, features = ["std"] }
+radroots_events = { path = "../lib/crates/events", default-features = false, features = ["serde", "std"] }
+radroots_events_codec = { path = "../lib/crates/events_codec", features = ["serde_json"] }
radroots_identity = { path = "../lib/crates/identity" }
radroots_local_events = { path = "../lib/crates/local_events", features = ["native"] }
radroots_nostr = { path = "../lib/crates/nostr", features = ["client"] }
diff --git a/crates/shared/sqlite/Cargo.toml b/crates/shared/sqlite/Cargo.toml
@@ -8,6 +8,9 @@ license.workspace = true
publish = false
[dependencies]
+radroots_core.workspace = true
+radroots_events.workspace = true
+radroots_events_codec.workspace = true
radroots_local_events.workspace = true
radroots_app_models.workspace = true
radroots_app_sync.workspace = true
diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs
@@ -2,7 +2,17 @@ use std::{fs, path::Path};
use radroots_app_models::{
FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary,
- FulfillmentWindowId, PickupLocationId, ProductId, ProductStatus,
+ FulfillmentWindowId, OrderId, PickupLocationId, ProductId, ProductStatus,
+};
+use radroots_events::{
+ RadrootsNostrEvent,
+ kinds::{KIND_TRADE_ORDER_REQUEST, KIND_TRADE_ORDER_RESPONSE},
+ trade::{
+ RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderRequested,
+ },
+};
+use radroots_events_codec::trade::{
+ active_trade_order_decision_from_event, active_trade_order_request_from_event,
};
use radroots_local_events::{
LocalEventRecord, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus,
@@ -21,6 +31,8 @@ const APP_LOCAL_INTEROP_CURSOR_ID: &str = "radroots_app_sqlite_projection_v1";
const KIND_FARM: i64 = 30340;
const KIND_LISTING: i64 = 30402;
const KIND_LISTING_DRAFT: i64 = 30403;
+const KIND_ORDER_REQUEST: i64 = KIND_TRADE_ORDER_REQUEST as i64;
+const KIND_ORDER_DECISION: i64 = KIND_TRADE_ORDER_RESPONSE as i64;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AppLocalInteropImportReport {
@@ -308,6 +320,8 @@ impl<'a> AppLocalInteropRepository<'a> {
match record.event_kind {
Some(KIND_FARM) => self.import_signed_farm(record),
Some(KIND_LISTING | KIND_LISTING_DRAFT) => self.import_signed_listing(record),
+ Some(KIND_ORDER_REQUEST) => self.import_signed_order_request(record),
+ Some(KIND_ORDER_DECISION) => self.import_signed_order_decision(record),
_ => Ok(Some(ProjectionRecord {
kind: "signed_event",
projected_id: record.event_id.clone(),
@@ -639,6 +653,206 @@ impl<'a> AppLocalInteropRepository<'a> {
}))
}
+ fn import_signed_order_request(
+ &self,
+ record: &LocalEventRecord,
+ ) -> Result<Option<ProjectionRecord>, AppSqliteError> {
+ let Some(event) = signed_event_from_record(record)? else {
+ return Ok(Some(signed_event_projection(record)));
+ };
+ let Ok(envelope) = active_trade_order_request_from_event(&event) else {
+ return Ok(Some(signed_event_projection(record)));
+ };
+ self.upsert_order_request(record, &envelope.payload)?;
+ Ok(Some(signed_event_projection(record)))
+ }
+
+ fn import_signed_order_decision(
+ &self,
+ record: &LocalEventRecord,
+ ) -> Result<Option<ProjectionRecord>, AppSqliteError> {
+ let Some(event) = signed_event_from_record(record)? else {
+ return Ok(Some(signed_event_projection(record)));
+ };
+ let Ok(envelope) = active_trade_order_decision_from_event(&event) else {
+ return Ok(Some(signed_event_projection(record)));
+ };
+ self.apply_order_decision(&envelope.payload)?;
+ Ok(Some(signed_event_projection(record)))
+ }
+
+ fn upsert_order_request(
+ &self,
+ record: &LocalEventRecord,
+ payload: &RadrootsTradeOrderRequested,
+ ) -> Result<OrderId, AppSqliteError> {
+ let existing_listing =
+ self.existing_listing_projection(Some(payload.listing_addr.as_str()))?;
+ let farm_id = if let Some(existing_listing) = existing_listing.as_ref() {
+ existing_listing.farm_id
+ } else {
+ deterministic_farm_id(
+ Some(payload.seller_pubkey.as_str()),
+ payload.listing_addr.as_str(),
+ )
+ };
+ self.ensure_farm_exists(farm_id)?;
+ let order_id = projected_order_id(payload.order_id.as_str(), payload.buyer_pubkey.as_str());
+ let order_number = existing_order_number(self.connection, order_id)?
+ .unwrap_or_else(|| deterministic_order_number(payload.order_id.as_str()));
+ self.connection
+ .execute(
+ "INSERT INTO orders (
+ id,
+ farm_id,
+ fulfillment_window_id,
+ order_number,
+ customer_display_name,
+ status,
+ updated_at,
+ buyer_context_key,
+ buyer_email,
+ buyer_phone,
+ buyer_order_note
+ ) VALUES (?1, ?2, null, ?3, ?4, 'needs_action', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), ?5, '', '', '')
+ ON CONFLICT(id) DO UPDATE SET
+ farm_id = excluded.farm_id,
+ order_number = excluded.order_number,
+ customer_display_name = excluded.customer_display_name,
+ status = CASE
+ WHEN orders.status IN ('scheduled', 'packed', 'completed', 'refunded')
+ THEN orders.status
+ ELSE excluded.status
+ END,
+ buyer_context_key = coalesce(orders.buyer_context_key, excluded.buyer_context_key),
+ updated_at = excluded.updated_at",
+ params![
+ order_id.to_string(),
+ farm_id.to_string(),
+ order_number.as_str(),
+ order_customer_display_name(payload.buyer_pubkey.as_str()),
+ order_buyer_context_key(record, payload.buyer_pubkey.as_str()),
+ ],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "upsert local interop order request",
+ source,
+ })?;
+ self.replace_order_request_lines(order_id, payload, existing_listing.as_ref(), record)?;
+ Ok(order_id)
+ }
+
+ fn apply_order_decision(
+ &self,
+ payload: &RadrootsTradeOrderDecisionEvent,
+ ) -> Result<(), AppSqliteError> {
+ let order_id = projected_order_id(payload.order_id.as_str(), payload.buyer_pubkey.as_str());
+ match &payload.decision {
+ RadrootsTradeOrderDecision::Accepted { .. } => {
+ self.connection
+ .execute(
+ "UPDATE orders
+ SET status = CASE
+ WHEN status IN ('packed', 'completed', 'refunded') THEN status
+ ELSE 'scheduled'
+ END,
+ updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
+ WHERE id = ?1",
+ params![order_id.to_string()],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "apply local interop order decision",
+ source,
+ })?;
+ }
+ RadrootsTradeOrderDecision::Declined { .. } => {}
+ }
+ Ok(())
+ }
+
+ fn replace_order_request_lines(
+ &self,
+ order_id: OrderId,
+ payload: &RadrootsTradeOrderRequested,
+ existing_listing: Option<&ExistingListingProjection>,
+ record: &LocalEventRecord,
+ ) -> Result<(), AppSqliteError> {
+ self.connection
+ .execute(
+ "DELETE FROM order_lines WHERE order_id = ?1",
+ params![order_id.to_string()],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "replace local interop order lines",
+ source,
+ })?;
+ for (index, item) in payload.items.iter().enumerate() {
+ let economics_item = payload
+ .economics
+ .items
+ .iter()
+ .find(|candidate| candidate.bin_id == item.bin_id);
+ let unit_label = economics_item
+ .map(|item| item.quantity_unit.to_string())
+ .or_else(|| existing_listing.map(|listing| listing.unit_label.clone()))
+ .unwrap_or_else(|| "item".to_owned());
+ let unit_price_minor_units = economics_item.and_then(|item| {
+ parse_decimal_minor_units(item.unit_price_amount.to_string().as_str())
+ });
+ let price_currency = economics_item
+ .map(|item| item.unit_price_currency.to_string())
+ .unwrap_or_else(|| payload.economics.currency.to_string());
+ let title = existing_listing
+ .map(|listing| listing.title.clone())
+ .unwrap_or_else(|| item.bin_id.clone());
+ self.connection
+ .execute(
+ "INSERT INTO order_lines (
+ id,
+ order_id,
+ title,
+ quantity_value,
+ quantity_unit_label,
+ quantity_display,
+ listing_bin_id,
+ unit_price_minor_units,
+ price_currency,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ listing_relays_json,
+ seller_pubkey,
+ sort_index
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, null, ?13, ?14)",
+ params![
+ format!(
+ "{}:{}",
+ order_id,
+ order_line_product_id(payload, existing_listing, item)
+ ),
+ order_id.to_string(),
+ title.as_str(),
+ i64::from(item.bin_count),
+ unit_label.as_str(),
+ format_quantity_display(item.bin_count, unit_label.as_str()),
+ item.bin_id.as_str(),
+ unit_price_minor_units,
+ price_currency.as_str(),
+ existing_listing.and_then(|listing| listing.farm_key.as_deref()),
+ payload.listing_addr.as_str(),
+ listing_event_id_from_order_record(record).as_deref(),
+ payload.seller_pubkey.as_str(),
+ index as i64,
+ ],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "insert local interop order line",
+ source,
+ })?;
+ }
+ Ok(())
+ }
+
fn upsert_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
self.connection
.execute(
@@ -1013,10 +1227,16 @@ impl<'a> AppLocalInteropRepository<'a> {
else {
return Ok(None);
};
- let Some((product_id, farm_id)) = self
+ let Some((product_id, farm_id, title, unit_label, listing_bin_id, farm_key)) = self
.connection
.query_row(
- "SELECT products.id, products.farm_id
+ "SELECT
+ products.id,
+ products.farm_id,
+ products.title,
+ products.unit_label,
+ products.listing_bin_id,
+ local_interop_imports.farm_key
FROM local_interop_imports
JOIN products ON products.id = local_interop_imports.projected_id
WHERE local_interop_imports.projected_kind = 'listing'
@@ -1025,7 +1245,16 @@ impl<'a> AppLocalInteropRepository<'a> {
ORDER BY local_interop_imports.local_seq DESC
LIMIT 1",
[listing_addr],
- |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
+ |row| {
+ Ok((
+ row.get::<_, String>(0)?,
+ row.get::<_, String>(1)?,
+ row.get::<_, String>(2)?,
+ row.get::<_, String>(3)?,
+ row.get::<_, Option<String>>(4)?,
+ row.get::<_, Option<String>>(5)?,
+ ))
+ },
)
.optional()
.map_err(|source| AppSqliteError::Query {
@@ -1046,6 +1275,10 @@ impl<'a> AppLocalInteropRepository<'a> {
.map_err(|_| AppSqliteError::InvalidProjection {
reason: "existing listing projection farm id must parse",
})?,
+ title,
+ unit_label,
+ listing_bin_id,
+ farm_key,
}))
}
@@ -1189,10 +1422,14 @@ struct ProductProjection {
listing_bin_id: Option<String>,
}
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq)]
struct ExistingListingProjection {
product_id: ProductId,
farm_id: FarmId,
+ title: String,
+ unit_label: String,
+ listing_bin_id: Option<String>,
+ farm_key: Option<String>,
}
fn deterministic_farm_id(owner_pubkey: Option<&str>, farm_key: &str) -> FarmId {
@@ -1266,6 +1503,179 @@ fn parse_app_d_tag_uuid(value: &str) -> Option<Uuid> {
}
}
+fn signed_event_projection(record: &LocalEventRecord) -> ProjectionRecord {
+ ProjectionRecord {
+ kind: "signed_event",
+ projected_id: record.event_id.clone(),
+ }
+}
+
+fn signed_event_from_record(
+ record: &LocalEventRecord,
+) -> Result<Option<RadrootsNostrEvent>, AppSqliteError> {
+ let Some(id) = record
+ .event_id
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ else {
+ return Ok(None);
+ };
+ let Some(author) = record
+ .event_pubkey
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ else {
+ return Ok(None);
+ };
+ let Some(kind) = record.event_kind.and_then(|kind| u32::try_from(kind).ok()) else {
+ return Ok(None);
+ };
+ let Some(created_at) = record
+ .event_created_at
+ .and_then(|created_at| u32::try_from(created_at).ok())
+ else {
+ return Ok(None);
+ };
+ let Some(sig) = record
+ .event_sig
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ else {
+ return Ok(None);
+ };
+ let Some(tags) = record.event_tags_json.as_ref().and_then(tags_from_json) else {
+ return Ok(None);
+ };
+ Ok(Some(RadrootsNostrEvent {
+ id: id.to_owned(),
+ author: author.to_owned(),
+ created_at,
+ kind,
+ tags,
+ content: record.event_content.clone().unwrap_or_default(),
+ sig: sig.to_owned(),
+ }))
+}
+
+fn tags_from_json(value: &Value) -> Option<Vec<Vec<String>>> {
+ value.as_array().map(|tags| {
+ tags.iter()
+ .filter_map(|tag| {
+ tag.as_array().map(|values| {
+ values
+ .iter()
+ .filter_map(|value| value.as_str().map(str::to_owned))
+ .collect::<Vec<_>>()
+ })
+ })
+ .collect::<Vec<_>>()
+ })
+}
+
+fn projected_order_id(order_id: &str, buyer_pubkey: &str) -> OrderId {
+ order_id.parse().unwrap_or_else(|_| {
+ OrderId::from(deterministic_uuid(
+ "radroots-cli-order",
+ Some(buyer_pubkey),
+ order_id,
+ ))
+ })
+}
+
+fn order_line_product_id(
+ payload: &RadrootsTradeOrderRequested,
+ existing_listing: Option<&ExistingListingProjection>,
+ item: &radroots_events::trade::RadrootsTradeOrderItem,
+) -> ProductId {
+ if let Some(existing_listing) = existing_listing
+ && existing_listing
+ .listing_bin_id
+ .as_deref()
+ .is_none_or(|listing_bin_id| listing_bin_id == item.bin_id)
+ {
+ return existing_listing.product_id;
+ }
+ let product_key = format!("{}:{}", payload.listing_addr, item.bin_id);
+ deterministic_product_id(Some(payload.seller_pubkey.as_str()), product_key.as_str())
+}
+
+fn deterministic_order_number(order_id: &str) -> String {
+ let trimmed = order_id.trim();
+ let suffix = trimmed
+ .chars()
+ .filter(|ch| ch.is_ascii_alphanumeric())
+ .take(8)
+ .collect::<String>();
+ if suffix.is_empty() {
+ "R-RELAY".to_owned()
+ } else {
+ format!("R-{suffix}")
+ }
+}
+
+fn existing_order_number(
+ connection: &Connection,
+ order_id: OrderId,
+) -> Result<Option<String>, AppSqliteError> {
+ connection
+ .query_row(
+ "SELECT order_number FROM orders WHERE id = ?1 LIMIT 1",
+ params![order_id.to_string()],
+ |row| row.get::<_, String>(0),
+ )
+ .optional()
+ .map_err(|source| AppSqliteError::Query {
+ operation: "load existing local interop order number",
+ source,
+ })
+}
+
+fn order_customer_display_name(buyer_pubkey: &str) -> String {
+ let prefix = buyer_pubkey.trim().chars().take(12).collect::<String>();
+ if prefix.is_empty() {
+ "Relay buyer".to_owned()
+ } else {
+ format!("Relay buyer {prefix}")
+ }
+}
+
+fn order_buyer_context_key(record: &LocalEventRecord, buyer_pubkey: &str) -> String {
+ if record.source_runtime == SourceRuntime::App
+ && record
+ .event_pubkey
+ .as_deref()
+ .map(str::trim)
+ .is_some_and(|event_pubkey| event_pubkey == buyer_pubkey.trim())
+ && let Some(owner_account_id) = record
+ .owner_account_id
+ .as_deref()
+ .map(str::trim)
+ .filter(|owner_account_id| !owner_account_id.is_empty())
+ {
+ return format!("account:{owner_account_id}");
+ }
+ format!("nostr:{}", buyer_pubkey.trim())
+}
+
+fn format_quantity_display(quantity: u32, unit_label: &str) -> String {
+ let unit_label = unit_label.trim();
+ if unit_label.is_empty() {
+ quantity.to_string()
+ } else {
+ format!("{quantity} {unit_label}")
+ }
+}
+
+fn listing_event_id_from_order_record(record: &LocalEventRecord) -> Option<String> {
+ record
+ .event_tags_json
+ .as_ref()
+ .and_then(|tags| tag_index_value(Some(tags), "listing_event", 1))
+}
+
fn base64_url_digit(byte: u8) -> Option<u8> {
match byte {
b'A'..=b'Z' => Some(byte - b'A'),
@@ -1576,7 +1986,26 @@ fn farm_readiness_from_storage_key(readiness: &str) -> Result<FarmReadiness, App
mod tests {
use std::collections::BTreeSet;
- use radroots_app_models::{FarmOrderMethod, ProductAvailabilityState};
+ use radroots_app_models::{
+ BuyerContext, BuyerOrderStatus, FarmOrderMethod, OrderStatus, OrdersFilter,
+ OrdersScreenQueryState, ProductAvailabilityState,
+ };
+ use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
+ };
+ use radroots_events::{
+ RadrootsNostrEvent, RadrootsNostrEventPtr,
+ trade::{
+ RadrootsTradeInventoryCommitment, RadrootsTradeOrderDecision,
+ RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem,
+ RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem,
+ RadrootsTradeOrderRequested, RadrootsTradePricingBasis,
+ },
+ };
+ use radroots_events_codec::{
+ trade::{active_trade_order_decision_event_build, active_trade_order_request_event_build},
+ wire::WireEventParts,
+ };
use radroots_local_events::{
LocalEventRecordInput, LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily,
LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
@@ -1586,7 +2015,10 @@ mod tests {
use serde_json::json;
use uuid::Uuid;
- use super::{KIND_FARM, KIND_LISTING, deterministic_farm_id, deterministic_product_id};
+ use super::{
+ KIND_FARM, KIND_LISTING, KIND_ORDER_REQUEST, deterministic_farm_id,
+ deterministic_product_id, projected_order_id,
+ };
use crate::{AppSqliteStore, DatabaseTarget};
fn local_events_store() -> LocalEventsStore<SqliteExecutor> {
@@ -1951,6 +2383,419 @@ mod tests {
.expect("seed origin product");
}
+ fn decimal(raw: &str) -> RadrootsCoreDecimal {
+ raw.parse().expect("valid decimal")
+ }
+
+ fn usd(raw: &str) -> RadrootsCoreMoney {
+ RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD)
+ }
+
+ fn listing_event_ptr(event_id: &str) -> RadrootsNostrEventPtr {
+ RadrootsNostrEventPtr {
+ id: event_id.to_owned(),
+ relays: Some("ws://127.0.0.1:1234/".to_owned()),
+ }
+ }
+
+ fn order_request_payload(
+ order_id: &str,
+ listing_addr: &str,
+ buyer_pubkey: &str,
+ seller_pubkey: &str,
+ ) -> RadrootsTradeOrderRequested {
+ RadrootsTradeOrderRequested {
+ order_id: order_id.to_owned(),
+ listing_addr: listing_addr.to_owned(),
+ buyer_pubkey: buyer_pubkey.to_owned(),
+ seller_pubkey: seller_pubkey.to_owned(),
+ items: vec![RadrootsTradeOrderItem {
+ bin_id: "bin-1".to_owned(),
+ bin_count: 2,
+ }],
+ economics: RadrootsTradeOrderEconomics {
+ quote_id: format!("quote-{order_id}"),
+ quote_version: 1,
+ pricing_basis: RadrootsTradePricingBasis::ListingEvent,
+ currency: RadrootsCoreCurrency::USD,
+ items: vec![RadrootsTradeOrderEconomicItem {
+ bin_id: "bin-1".to_owned(),
+ bin_count: 2,
+ quantity_amount: decimal("1"),
+ quantity_unit: RadrootsCoreUnit::Each,
+ unit_price_amount: decimal("8"),
+ unit_price_currency: RadrootsCoreCurrency::USD,
+ line_subtotal: usd("16"),
+ }],
+ discounts: Vec::<RadrootsTradeOrderEconomicLine>::new(),
+ adjustments: Vec::<RadrootsTradeOrderEconomicLine>::new(),
+ subtotal: usd("16"),
+ discount_total: usd("0"),
+ adjustment_total: usd("0"),
+ total: usd("16"),
+ },
+ }
+ }
+
+ fn order_decision_payload(
+ order_id: &str,
+ listing_addr: &str,
+ buyer_pubkey: &str,
+ seller_pubkey: &str,
+ ) -> RadrootsTradeOrderDecisionEvent {
+ RadrootsTradeOrderDecisionEvent {
+ order_id: order_id.to_owned(),
+ listing_addr: listing_addr.to_owned(),
+ buyer_pubkey: buyer_pubkey.to_owned(),
+ seller_pubkey: seller_pubkey.to_owned(),
+ decision: RadrootsTradeOrderDecision::Accepted {
+ inventory_commitments: vec![RadrootsTradeInventoryCommitment {
+ bin_id: "bin-1".to_owned(),
+ bin_count: 2,
+ }],
+ },
+ }
+ }
+
+ fn event_from_parts(event_id: &str, author: &str, parts: WireEventParts) -> RadrootsNostrEvent {
+ RadrootsNostrEvent {
+ id: event_id.to_owned(),
+ author: author.to_owned(),
+ created_at: 1_777_665_600,
+ kind: parts.kind,
+ tags: parts.tags,
+ content: parts.content,
+ sig: format!("sig-{event_id}"),
+ }
+ }
+
+ fn signed_order_event_record(
+ record_id: &str,
+ event: &RadrootsNostrEvent,
+ listing_addr: &str,
+ source_runtime: SourceRuntime,
+ owner_account_id: Option<&str>,
+ ) -> LocalEventRecordInput {
+ LocalEventRecordInput {
+ record_id: record_id.to_owned(),
+ family: LocalRecordFamily::SignedEvent,
+ status: LocalRecordStatus::Published,
+ source_runtime,
+ created_at_ms: i64::from(event.created_at) * 1_000,
+ inserted_at_ms: i64::from(event.created_at) * 1_000 + 1,
+ owner_account_id: owner_account_id.map(str::to_owned),
+ owner_pubkey: Some(event.author.clone()),
+ farm_id: None,
+ listing_addr: Some(listing_addr.to_owned()),
+ local_work_json: None,
+ event_id: Some(event.id.clone()),
+ event_kind: Some(i64::from(event.kind)),
+ event_pubkey: Some(event.author.clone()),
+ event_created_at: Some(i64::from(event.created_at)),
+ event_tags_json: Some(json!(event.tags)),
+ event_content: Some(event.content.clone()),
+ event_sig: Some(event.sig.clone()),
+ raw_event_json: Some(json!({
+ "id": event.id,
+ "kind": event.kind,
+ "pubkey": event.author,
+ "created_at": event.created_at,
+ "tags": event.tags,
+ "content": event.content,
+ "sig": event.sig,
+ })),
+ outbox_status: PublishOutboxStatus::Acknowledged,
+ relay_set_fingerprint: Some("relay-set".to_owned()),
+ relay_delivery_json: Some(json!({
+ "state": "acknowledged",
+ "acknowledged_relays": ["ws://127.0.0.1:1234/"]
+ })),
+ }
+ }
+
+ #[test]
+ fn imports_signed_order_request_into_seller_order_projection() {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ let farm_key = "AAAAAAAAAAAAAAAAAAAAAA";
+ let listing_key = "AAAAAAAAAAAAAAAAAAAAAg";
+ let seller_pubkey = "seller-pubkey";
+ let buyer_pubkey = "buyer-pubkey";
+ let order_id_raw = "relay-order-1";
+ let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
+ events
+ .append_record(&signed_market_listing_record(
+ "order-visible-listing",
+ seller_pubkey,
+ farm_key,
+ listing_key,
+ "Order Visible Eggs",
+ "9",
+ "active",
+ "pickup",
+ "North barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ))
+ .expect("append signed listing");
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import signed listing");
+ let payload = order_request_payload(
+ order_id_raw,
+ listing_addr.as_str(),
+ buyer_pubkey,
+ seller_pubkey,
+ );
+ let parts =
+ active_trade_order_request_event_build(&listing_event_ptr("listing-event-1"), &payload)
+ .expect("build order request event");
+ let event = event_from_parts("order-request-event-1", buyer_pubkey, parts);
+ events
+ .append_record(&signed_order_event_record(
+ "cli:signed_event:order-request:1",
+ &event,
+ listing_addr.as_str(),
+ SourceRuntime::Cli,
+ None,
+ ))
+ .expect("append order request");
+
+ let report = app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import signed order request");
+ let farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let orders = app_store
+ .load_orders_list(
+ farm_id,
+ &OrdersScreenQueryState {
+ filter: OrdersFilter::All,
+ fulfillment_window_id: None,
+ },
+ )
+ .expect("load seller orders");
+ let detail = app_store
+ .load_order_detail(farm_id, order_id)
+ .expect("load order detail")
+ .expect("order detail");
+ let imported = app_store
+ .load_local_interop_records()
+ .expect("load imported records");
+ let buyer_context_key: String = app_store
+ .connection()
+ .query_row(
+ "SELECT buyer_context_key FROM orders WHERE id = ?1",
+ [order_id.to_string()],
+ |row| row.get(0),
+ )
+ .expect("load buyer context key");
+
+ assert_eq!(report.imported_records, 1);
+ assert!(
+ imported
+ .iter()
+ .any(|record| record.projected_kind == "signed_event"
+ && record.event_kind == Some(KIND_ORDER_REQUEST)
+ && record.event_id.as_deref() == Some("order-request-event-1"))
+ );
+ assert_eq!(orders.rows.len(), 1);
+ assert_eq!(orders.rows[0].order_id, order_id);
+ assert_eq!(orders.rows[0].status, OrderStatus::NeedsAction);
+ assert_eq!(
+ orders.rows[0].customer_display_name,
+ "Relay buyer buyer-pubkey"
+ );
+ assert_eq!(detail.items.len(), 1);
+ assert_eq!(detail.items[0].title, "Order Visible Eggs");
+ assert_eq!(detail.items[0].quantity_display, "2 each");
+ assert_eq!(buyer_context_key, "nostr:buyer-pubkey");
+ }
+
+ #[test]
+ fn app_origin_signed_order_request_and_decision_project_to_buyer_orders() {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ let farm_key = "CCCCCCCCCCCCCCCCCCCCCC";
+ let listing_key = "AAAAAAAAAAAAAAAAAAAAAg";
+ let seller_pubkey = "seller-pubkey";
+ let buyer_pubkey = "app-buyer-pubkey";
+ let order_id_raw = "app-relay-order-1";
+ let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
+ events
+ .append_record(&signed_market_listing_record(
+ "buyer-order-listing",
+ seller_pubkey,
+ farm_key,
+ listing_key,
+ "Buyer Order Eggs",
+ "9",
+ "active",
+ "pickup",
+ "North barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ))
+ .expect("append signed listing");
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import signed listing");
+ let request_payload = order_request_payload(
+ order_id_raw,
+ listing_addr.as_str(),
+ buyer_pubkey,
+ seller_pubkey,
+ );
+ let request_parts = active_trade_order_request_event_build(
+ &listing_event_ptr("buyer-order-listing-event"),
+ &request_payload,
+ )
+ .expect("build order request event");
+ let request_event =
+ event_from_parts("buyer-order-request-event", buyer_pubkey, request_parts);
+ events
+ .append_record(&signed_order_event_record(
+ "app:signed_event:order-request:buyer",
+ &request_event,
+ listing_addr.as_str(),
+ SourceRuntime::App,
+ Some("acct_buyer"),
+ ))
+ .expect("append app order request");
+
+ let request_report = app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import app order request");
+ let buyer_context = BuyerContext::account("acct_buyer");
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let buyer_orders = app_store
+ .load_buyer_orders(&buyer_context)
+ .expect("load buyer orders after request");
+
+ assert_eq!(request_report.imported_records, 1);
+ assert_eq!(buyer_orders.rows.len(), 1);
+ assert_eq!(buyer_orders.rows[0].order_id, order_id);
+ assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Placed);
+
+ let decision_payload = order_decision_payload(
+ order_id_raw,
+ listing_addr.as_str(),
+ buyer_pubkey,
+ seller_pubkey,
+ );
+ let decision_parts = active_trade_order_decision_event_build(
+ request_event.id.as_str(),
+ request_event.id.as_str(),
+ &decision_payload,
+ )
+ .expect("build order decision event");
+ let decision_event =
+ event_from_parts("buyer-order-decision-event", seller_pubkey, decision_parts);
+ events
+ .append_record(&signed_order_event_record(
+ "cli:signed_event:order-decision:buyer",
+ &decision_event,
+ listing_addr.as_str(),
+ SourceRuntime::Cli,
+ None,
+ ))
+ .expect("append order decision");
+
+ let decision_report = app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import order decision");
+ let buyer_orders = app_store
+ .load_buyer_orders(&buyer_context)
+ .expect("load buyer orders after decision");
+ let buyer_detail = app_store
+ .load_buyer_order_detail(&buyer_context, order_id)
+ .expect("load buyer order detail")
+ .expect("buyer order detail");
+ let seller_orders = app_store
+ .load_orders_list(
+ deterministic_farm_id(Some(seller_pubkey), farm_key),
+ &OrdersScreenQueryState {
+ filter: OrdersFilter::All,
+ fulfillment_window_id: None,
+ },
+ )
+ .expect("load seller orders after decision");
+
+ assert_eq!(decision_report.imported_records, 1);
+ assert_eq!(buyer_orders.rows.len(), 1);
+ assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Scheduled);
+ assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled);
+ assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled);
+ }
+
+ #[test]
+ fn malformed_order_event_remains_signed_event_evidence_without_projection() {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ events
+ .append_record(&LocalEventRecordInput {
+ record_id: "cli:signed_event:order-request:malformed".to_owned(),
+ family: LocalRecordFamily::SignedEvent,
+ status: LocalRecordStatus::Published,
+ source_runtime: SourceRuntime::Cli,
+ created_at_ms: 1100,
+ inserted_at_ms: 1101,
+ owner_account_id: None,
+ owner_pubkey: Some("buyer-pubkey".to_owned()),
+ farm_id: None,
+ listing_addr: Some("30402:seller-pubkey:listing-key".to_owned()),
+ local_work_json: None,
+ event_id: Some("malformed-order-event".to_owned()),
+ event_kind: Some(KIND_ORDER_REQUEST),
+ event_pubkey: Some("buyer-pubkey".to_owned()),
+ event_created_at: Some(1100),
+ event_tags_json: Some(json!([["d", "bad-order"]])),
+ event_content: Some("not-json".to_owned()),
+ event_sig: Some("signature".to_owned()),
+ raw_event_json: Some(json!({
+ "id": "malformed-order-event",
+ "kind": KIND_ORDER_REQUEST,
+ "pubkey": "buyer-pubkey",
+ "content": "not-json"
+ })),
+ outbox_status: PublishOutboxStatus::Acknowledged,
+ relay_set_fingerprint: Some("relay-set".to_owned()),
+ relay_delivery_json: Some(json!({
+ "state": "acknowledged",
+ "acknowledged_relays": ["ws://127.0.0.1:1234/"]
+ })),
+ })
+ .expect("append malformed order event");
+
+ let report = app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import malformed order event");
+ let imported = app_store
+ .load_local_interop_records()
+ .expect("load imported records");
+ let order_count: i64 = app_store
+ .connection()
+ .query_row("SELECT COUNT(*) FROM orders", [], |row| row.get(0))
+ .expect("load order count");
+
+ assert_eq!(report.imported_records, 1);
+ assert_eq!(report.skipped_records, 0);
+ assert_eq!(imported.len(), 1);
+ assert_eq!(imported[0].projected_kind, "signed_event");
+ assert_eq!(
+ imported[0].event_id.as_deref(),
+ Some("malformed-order-event")
+ );
+ assert_eq!(order_count, 0);
+ }
+
#[test]
fn imports_cli_local_work_into_app_farm_and_product_projection() {
let app_store =