commit e7a4cbbf664034df06612331182c4aa57a2ffd11
parent b33ce0171c1fb34ae574375b054d9f81a8d08e00
Author: triesap <tyson@radroots.org>
Date: Tue, 26 May 2026 02:07:55 +0000
orders: publish seller decisions
Diffstat:
1 file changed, 194 insertions(+), 13 deletions(-)
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -701,6 +701,24 @@ impl DesktopAppRuntime {
)
}
+ pub fn publish_order_accept(&self, order_id: OrderId) -> Result<bool, AppSqliteError> {
+ self.lock_state_mut()
+ .publish_seller_order_decision(order_id, AppSellerOrderDecisionCommand::Accept)
+ }
+
+ pub fn publish_order_decline(
+ &self,
+ order_id: OrderId,
+ reason: &str,
+ ) -> Result<bool, AppSqliteError> {
+ self.lock_state_mut().publish_seller_order_decision(
+ order_id,
+ AppSellerOrderDecisionCommand::Decline {
+ reason: reason.to_owned(),
+ },
+ )
+ }
+
pub fn start_order_recovery(
&self,
order_id: OrderId,
@@ -2378,6 +2396,23 @@ impl DesktopAppRuntimeState {
Ok(payload)
}
+ fn publish_seller_order_decision(
+ &mut self,
+ order_id: OrderId,
+ command: AppSellerOrderDecisionCommand,
+ ) -> Result<bool, AppSqliteError> {
+ let payload = self.prepare_seller_order_decision(order_id, command)?;
+ let operation = PendingSyncOperation::from_publish_payload(
+ AppPublishPayload::OrderDecision(payload),
+ current_utc_timestamp(),
+ )
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "seller order decision publish payload must serialize",
+ })?;
+ let _ = self.enqueue_selected_account_sync_operation_once(operation)?;
+ self.attempt_sync(SyncTrigger::ManualRefresh)
+ }
+
fn start_order_recovery(
&mut self,
order_id: OrderId,
@@ -3725,6 +3760,12 @@ impl DesktopAppRuntimeState {
result: &AppSyncResult,
) -> Result<bool, AppSqliteError> {
self.record_published_sync_receipts(result.published_receipts.as_slice())?;
+ let receipt_import_changed = if result.published_receipts.is_empty() {
+ false
+ } else {
+ let report = self.import_shared_local_events()?;
+ report.imported_records > 0 || report.skipped_records > 0
+ };
{
let Some(sqlite_store) = self.sqlite_store.as_ref() else {
return Ok(false);
@@ -3761,7 +3802,7 @@ impl DesktopAppRuntimeState {
}
}
- self.refresh_selected_account_sync()
+ Ok(receipt_import_changed || self.refresh_selected_account_sync()?)
}
fn apply_sync_transport_error(
@@ -6007,10 +6048,17 @@ async fn publish_app_payload(
.map_err(|error| AppSyncTransportError::failed(error.to_string()))
}
AppPublishPayload::OrderDecision(payload) => {
- let _decision = order_decision_publish_payload_to_sdk_decision(payload);
- Err(AppSyncTransportError::failed(
- "order decision direct relay publish is not wired",
- ))
+ let decision = order_decision_publish_payload_to_sdk_decision(payload);
+ client
+ .trade()
+ .publish_order_decision_with_identity(
+ identity,
+ payload.request_event_id.as_str(),
+ payload.request_event_id.as_str(),
+ &decision,
+ )
+ .await
+ .map_err(|error| AppSyncTransportError::failed(error.to_string()))
}
}
}
@@ -8237,14 +8285,15 @@ mod tests {
HomeRoute,
};
use radroots_app_sync::{
- AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderDecisionPayload,
- AppOrderRequestItemPayload, AppOrderRequestPublishPayload, AppPublishContext,
- AppPublishPayload, AppPublishedOperationReceipt, AppRelayIngestScopeFreshness,
- AppRelayIngestScopeStatus, AppSyncRequest, AppSyncResult, AppSyncRunStatus,
- AppSyncTransport, AppSyncTransportError, PendingSyncOperation, PendingSyncOperationState,
- RecordedAppSyncTransport, SyncAggregateRef, SyncCheckpointState, SyncCheckpointStatus,
- SyncConflict, SyncConflictKind, SyncConflictResolutionStatus, SyncConflictSeverity,
- SyncOperationKind, SyncTrigger,
+ AppFarmProfilePublishPayload, AppListingPublishPayload,
+ AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload,
+ AppOrderDecisionPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload,
+ AppPublishContext, AppPublishPayload, AppPublishedOperationReceipt,
+ AppRelayIngestScopeFreshness, AppRelayIngestScopeStatus, AppSyncRequest, AppSyncResult,
+ AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, PendingSyncOperation,
+ PendingSyncOperationState, RecordedAppSyncTransport, SyncAggregateRef, SyncCheckpointState,
+ SyncCheckpointStatus, SyncConflict, SyncConflictKind, SyncConflictResolutionStatus,
+ SyncConflictSeverity, SyncOperationKind, SyncTrigger,
};
use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
@@ -8474,6 +8523,21 @@ mod tests {
shared
}
+ fn install_direct_relay_sync_transport(runtime: &DesktopAppRuntime, relay: &ThreadedAckRelay) {
+ let accounts_manager = runtime
+ .lock_state()
+ .accounts_manager
+ .as_ref()
+ .expect("accounts manager")
+ .clone();
+ runtime.lock_state_mut().nostr_relay_urls = vec![relay.url().to_owned()];
+ runtime.lock_state_mut().sync_transport =
+ Box::new(SdkDirectRelayAppSyncTransport::with_relay_urls(
+ accounts_manager,
+ vec![relay.url().to_owned()],
+ ));
+ }
+
#[test]
fn runtime_direct_relay_transport_publishes_typed_farm_work() {
let relay_a = ThreadedAckRelay::spawn();
@@ -8540,6 +8604,60 @@ mod tests {
}
#[test]
+ fn runtime_direct_relay_transport_publishes_typed_order_decision_work() {
+ let relay = ThreadedAckRelay::spawn();
+ let manager = RadrootsNostrAccountsManager::new_in_memory();
+ let account_id = manager
+ .generate_identity(Some("Seller".to_owned()), true)
+ .expect("local signing account should generate");
+ let identity = manager
+ .get_signing_identity(&account_id)
+ .expect("seller signer lookup should succeed")
+ .expect("seller account should have local signer");
+ let buyer_pubkey = "1111111111111111111111111111111111111111111111111111111111111111";
+ let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload {
+ context: AppPublishContext::new(account_id.to_string(), "seller_order_decision"),
+ app_order_id: OrderId::new(),
+ farm_id: FarmId::new(),
+ trade_order_id: "order-1".to_owned(),
+ request_event_id: "order-request-event-1".to_owned(),
+ listing_event_id: Some("listing-event-1".to_owned()),
+ listing_addr: format!("30402:{}:listing-key", identity.public_key_hex()),
+ buyer_pubkey: buyer_pubkey.to_owned(),
+ seller_pubkey: identity.public_key_hex(),
+ decision: AppOrderDecisionPayload::Accepted {
+ inventory_commitments: vec![AppOrderDecisionInventoryCommitment {
+ bin_id: "bin-1".to_owned(),
+ bin_count: 2,
+ }],
+ },
+ });
+ let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
+ .expect("typed order decision publish work should serialize");
+ let mut transport =
+ SdkDirectRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
+
+ let result = transport
+ .sync(AppSyncRequest {
+ trigger: SyncTrigger::ManualRefresh,
+ checkpoint: SyncCheckpointStatus::never_synced(),
+ pending_operations: vec![operation],
+ known_conflicts: Vec::new(),
+ })
+ .expect("direct relay order decision publish should succeed");
+
+ assert_eq!(result.run_status, AppSyncRunStatus::Succeeded);
+ assert_eq!(result.pushed_operation_count, 1);
+ assert_eq!(result.published_receipts.len(), 1);
+ assert_eq!(result.published_receipts[0].event_kind, 3423);
+ assert_eq!(
+ result.published_receipts[0].event_pubkey,
+ identity.public_key_hex()
+ );
+ assert_eq!(relay.event_count(), 1);
+ }
+
+ #[test]
fn runtime_configured_relay_sync_triggers_ingest_listing_into_fresh_buyer_projection() {
let relay = ThreadedAckRelay::spawn();
let product_id = publish_relay_ingest_listing_fixture(&relay);
@@ -12755,6 +12873,54 @@ mod tests {
}
#[test]
+ fn runtime_publishes_seller_order_accept_and_projects_signed_evidence() {
+ let relay = ThreadedAckRelay::spawn();
+ let (runtime, paths, order_id, _product_id, seller_pubkey, _buyer_pubkey) =
+ seller_order_decision_runtime("seller_order_accept_publish", 6, 2);
+ install_direct_relay_sync_transport(&runtime, &relay);
+
+ assert!(
+ runtime
+ .publish_order_accept(order_id)
+ .expect("seller order accept should publish")
+ );
+
+ assert_eq!(persisted_order_status(&runtime, order_id), "scheduled");
+ assert_eq!(relay.event_count(), 1);
+ assert!(shared_local_event_records(&paths).iter().any(|record| {
+ record.family == LocalRecordFamily::SignedEvent
+ && record.event_kind == Some(3423)
+ && record.event_pubkey.as_deref() == Some(seller_pubkey.as_str())
+ }));
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
+ fn runtime_publishes_seller_order_decline_and_projects_signed_evidence() {
+ let relay = ThreadedAckRelay::spawn();
+ let (runtime, paths, order_id, _product_id, seller_pubkey, _buyer_pubkey) =
+ seller_order_decision_runtime("seller_order_decline_publish", 6, 2);
+ install_direct_relay_sync_transport(&runtime, &relay);
+
+ assert!(
+ runtime
+ .publish_order_decline(order_id, "not available")
+ .expect("seller order decline should publish")
+ );
+
+ assert_eq!(persisted_order_status(&runtime, order_id), "declined");
+ assert_eq!(relay.event_count(), 1);
+ assert!(shared_local_event_records(&paths).iter().any(|record| {
+ record.family == LocalRecordFamily::SignedEvent
+ && record.event_kind == Some(3423)
+ && record.event_pubkey.as_deref() == Some(seller_pubkey.as_str())
+ }));
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
fn runtime_places_supported_buyer_order_into_shared_local_events() {
let (runtime, paths) = bootstrapped_runtime("buyer_order_local_event");
assert!(
@@ -17045,6 +17211,21 @@ mod tests {
.expect("shared local records should list")
}
+ fn persisted_order_status(runtime: &DesktopAppRuntime, order_id: OrderId) -> String {
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .connection()
+ .query_row(
+ "select status from orders where id = ?1 limit 1",
+ [order_id.to_string()],
+ |row| row.get::<_, String>(0),
+ )
+ .expect("order status should load")
+ }
+
fn pending_order_sync_payloads(
runtime: &DesktopAppRuntime,
account_id: &str,