app

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

commit 280ffcdcf3b5f301db4a5127f94855e6e6d93bf7
parent d620a3a81a2d8a4abbd0593140f572e97f9baeb2
Author: triesap <tyson@radroots.org>
Date:   Wed,  3 Jun 2026 16:54:02 -0700

sync: use shared fulfillment state

Diffstat:
Mcrates/desktop/src/runtime.rs | 61+++++++++++++++++++++++++------------------------------------
Mcrates/sync/src/lib.rs | 10+++++-----
Mcrates/sync/src/publish.rs | 76++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
3 files changed, 78 insertions(+), 69 deletions(-)

diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -34,12 +34,11 @@ use radroots_app_state::{ use radroots_app_sync::{ AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload, AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload, AppOrderDecisionPublishPayload, - AppOrderFulfillmentPublishPayload, AppOrderFulfillmentPublishStatus, - AppOrderReceiptPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload, - AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload, - AppPublishContext, AppPublishPayload, AppPublishedOperationReceipt, - AppRelayIngestScopeFreshness, AppSyncProjection, AppSyncRequest, AppSyncResult, - AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, PendingSyncOperation, + AppOrderFulfillmentPublishPayload, AppOrderReceiptPublishPayload, AppOrderRequestItemPayload, + AppOrderRequestPublishPayload, AppOrderRevisionDecisionPublishPayload, + AppOrderRevisionProposalPublishPayload, AppPublishContext, AppPublishPayload, + AppPublishedOperationReceipt, AppRelayIngestScopeFreshness, AppSyncProjection, AppSyncRequest, + AppSyncResult, AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, PendingSyncOperation, SyncAggregateRef, SyncCheckpointStatus, SyncConflictSeverity, SyncOperationKind, SyncTrigger, }; use radroots_app_view::{ @@ -770,13 +769,15 @@ impl DesktopAppRuntime { ) -> Result<bool, AppSqliteError> { self.lock_state_mut().publish_seller_order_fulfillment( order_id, - AppOrderFulfillmentPublishStatus::ReadyForPickup, + RadrootsActiveTradeFulfillmentState::ReadyForPickup, ) } pub fn publish_order_delivered(&self, order_id: OrderId) -> Result<bool, AppSqliteError> { - self.lock_state_mut() - .publish_seller_order_fulfillment(order_id, AppOrderFulfillmentPublishStatus::Delivered) + self.lock_state_mut().publish_seller_order_fulfillment( + order_id, + RadrootsActiveTradeFulfillmentState::Delivered, + ) } pub fn publish_order_revision_proposal( @@ -2535,7 +2536,7 @@ impl DesktopAppRuntimeState { fn prepare_seller_order_fulfillment( &mut self, order_id: OrderId, - status: AppOrderFulfillmentPublishStatus, + status: RadrootsActiveTradeFulfillmentState, ) -> Result<AppOrderFulfillmentPublishPayload, AppSqliteError> { let _ = self.import_shared_local_events()?; let relay_urls = normalized_app_sync_relay_urls(&self.nostr_relay_urls).map_err(|_| { @@ -2621,9 +2622,14 @@ impl DesktopAppRuntimeState { }); }; let prev_event_id = match status { - AppOrderFulfillmentPublishStatus::ReadyForPickup - | AppOrderFulfillmentPublishStatus::Preparing - | AppOrderFulfillmentPublishStatus::OutForDelivery => { + RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled => { + return Err(AppSqliteError::InvalidProjection { + reason: "seller order fulfillment status must be publishable", + }); + } + RadrootsActiveTradeFulfillmentState::ReadyForPickup + | RadrootsActiveTradeFulfillmentState::Preparing + | RadrootsActiveTradeFulfillmentState::OutForDelivery => { if order_detail.status != OrderStatus::Scheduled { return Err(AppSqliteError::InvalidProjection { reason: "seller order fulfillment requires a scheduled order", @@ -2635,7 +2641,7 @@ impl DesktopAppRuntimeState { .map(|fulfillment| fulfillment.event_id.clone()) .unwrap_or_else(|| decision.event_id.clone()) } - AppOrderFulfillmentPublishStatus::Delivered => { + RadrootsActiveTradeFulfillmentState::Delivered => { if order_detail.status != OrderStatus::Packed { return Err(AppSqliteError::InvalidProjection { reason: "seller order delivery requires a ready order", @@ -2649,7 +2655,7 @@ impl DesktopAppRuntimeState { reason: "seller order delivery requires fulfillment evidence", })? } - AppOrderFulfillmentPublishStatus::SellerCancelled => lifecycle + RadrootsActiveTradeFulfillmentState::SellerCancelled => lifecycle .latest_fulfillment .as_ref() .map(|fulfillment| fulfillment.event_id.clone()) @@ -2678,7 +2684,7 @@ impl DesktopAppRuntimeState { fn publish_seller_order_fulfillment( &mut self, order_id: OrderId, - status: AppOrderFulfillmentPublishStatus, + status: RadrootsActiveTradeFulfillmentState, ) -> Result<bool, AppSqliteError> { let payload = self.prepare_seller_order_fulfillment(order_id, status)?; let operation = PendingSyncOperation::from_publish_payload( @@ -9468,23 +9474,7 @@ fn order_fulfillment_publish_payload_to_sdk_fulfillment( listing_addr: payload.listing_addr.clone(), buyer_pubkey: payload.buyer_pubkey.clone(), seller_pubkey: payload.seller_pubkey.clone(), - status: match payload.status { - AppOrderFulfillmentPublishStatus::Preparing => { - RadrootsActiveTradeFulfillmentState::Preparing - } - AppOrderFulfillmentPublishStatus::ReadyForPickup => { - RadrootsActiveTradeFulfillmentState::ReadyForPickup - } - AppOrderFulfillmentPublishStatus::OutForDelivery => { - RadrootsActiveTradeFulfillmentState::OutForDelivery - } - AppOrderFulfillmentPublishStatus::Delivered => { - RadrootsActiveTradeFulfillmentState::Delivered - } - AppOrderFulfillmentPublishStatus::SellerCancelled => { - RadrootsActiveTradeFulfillmentState::SellerCancelled - } - }, + status: payload.status, } } @@ -9613,8 +9603,7 @@ mod tests { AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload, AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload, AppOrderDecisionPublishPayload, AppOrderFulfillmentPublishPayload, - AppOrderFulfillmentPublishStatus, AppOrderReceiptPublishPayload, - AppOrderRequestItemPayload, AppOrderRequestPublishPayload, + AppOrderReceiptPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload, AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload, AppPublishContext, AppPublishPayload, AppPublishedOperationReceipt, AppRelayIngestScopeFreshness, AppRelayIngestScopeStatus, AppSyncRequest, AppSyncResult, @@ -10326,7 +10315,7 @@ mod tests { listing_addr: common.4.clone(), buyer_pubkey: common.5.clone(), seller_pubkey: common.6.clone(), - status: AppOrderFulfillmentPublishStatus::ReadyForPickup, + status: RadrootsActiveTradeFulfillmentState::ReadyForPickup, }); let receipt = AppPublishPayload::OrderReceipt(AppOrderReceiptPublishPayload { context: AppPublishContext::new(buyer_account_id.to_string(), "buyer_order_receipt"), diff --git a/crates/sync/src/lib.rs b/crates/sync/src/lib.rs @@ -5,11 +5,11 @@ mod publish; pub use publish::{ AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload, AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload, AppOrderDecisionPublishPayload, - AppOrderFulfillmentPublishPayload, AppOrderFulfillmentPublishStatus, - AppOrderReceiptPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload, - AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload, - AppPublishContext, AppPublishPayload, AppPublishPayloadJsonError, AppPublishValidationFailure, - AppPublishValidationFailureSet, AppPublishWorkKind, + AppOrderFulfillmentPublishPayload, AppOrderReceiptPublishPayload, AppOrderRequestItemPayload, + AppOrderRequestPublishPayload, AppOrderRevisionDecisionPublishPayload, + AppOrderRevisionProposalPublishPayload, AppPublishContext, AppPublishPayload, + AppPublishPayloadJsonError, AppPublishValidationFailure, AppPublishValidationFailureSet, + AppPublishWorkKind, }; use radroots_app_view::{FarmId, FulfillmentWindowId, OrderId, ProductId}; diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs @@ -3,7 +3,8 @@ use radroots_app_view::{ }; use radroots_sdk::SdkTransportMode; use radroots_sdk::trade::{ - RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRevisionDecision, + RadrootsActiveTradeFulfillmentState, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, + RadrootsTradeOrderRevisionDecision, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -213,16 +214,6 @@ pub struct AppOrderRevisionDecisionPublishPayload { pub decision: RadrootsTradeOrderRevisionDecision, } -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AppOrderFulfillmentPublishStatus { - Preparing, - ReadyForPickup, - OutForDelivery, - Delivered, - SellerCancelled, -} - #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct AppOrderFulfillmentPublishPayload { pub context: AppPublishContext, @@ -234,7 +225,7 @@ pub struct AppOrderFulfillmentPublishPayload { pub listing_addr: String, pub buyer_pubkey: String, pub seller_pubkey: String, - pub status: AppOrderFulfillmentPublishStatus, + pub status: RadrootsActiveTradeFulfillmentState, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -542,16 +533,21 @@ impl AppPublishPayload { failures.push(AppPublishValidationFailure::MissingOrderCancellationReason); } } - Self::OrderFulfillment(payload) => validate_lifecycle_order_fields( - &payload.context, - payload.trade_order_id.as_str(), - payload.request_event_id.as_str(), - payload.prev_event_id.as_str(), - payload.listing_addr.as_str(), - payload.buyer_pubkey.as_str(), - payload.seller_pubkey.as_str(), - &mut failures, - ), + Self::OrderFulfillment(payload) => { + validate_lifecycle_order_fields( + &payload.context, + payload.trade_order_id.as_str(), + payload.request_event_id.as_str(), + payload.prev_event_id.as_str(), + payload.listing_addr.as_str(), + payload.buyer_pubkey.as_str(), + payload.seller_pubkey.as_str(), + &mut failures, + ); + if !payload.status.is_publishable_update() { + failures.push(AppPublishValidationFailure::InvalidOrderFulfillmentStatus); + } + } Self::OrderReceipt(payload) => { validate_lifecycle_order_fields( &payload.context, @@ -664,6 +660,7 @@ pub enum AppPublishValidationFailure { MissingOrderRevisionReason, MissingOrderRevisionDecisionReason, MissingOrderCancellationReason, + InvalidOrderFulfillmentStatus, MissingOrderReceiptIssue, UnexpectedOrderReceiptIssue, } @@ -705,6 +702,7 @@ impl AppPublishValidationFailure { Self::MissingOrderRevisionReason => "missing_order_revision_reason", Self::MissingOrderRevisionDecisionReason => "missing_order_revision_decision_reason", Self::MissingOrderCancellationReason => "missing_order_cancellation_reason", + Self::InvalidOrderFulfillmentStatus => "invalid_order_fulfillment_status", Self::MissingOrderReceiptIssue => "missing_order_receipt_issue", Self::UnexpectedOrderReceiptIssue => "unexpected_order_receipt_issue", } @@ -760,8 +758,7 @@ mod tests { use super::{ AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload, AppOrderDecisionPayload, AppOrderDecisionPublishPayload, AppOrderFulfillmentPublishPayload, - AppOrderFulfillmentPublishStatus, AppOrderReceiptPublishPayload, - AppOrderRequestItemPayload, AppOrderRequestPublishPayload, + AppOrderReceiptPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload, AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload, AppPublishContext, AppPublishPayload, AppPublishValidationFailure, AppPublishWorkKind, }; @@ -770,7 +767,8 @@ mod tests { }; use radroots_app_view::{FarmId, FarmReadiness, OrderId, ProductId, ProductStatus}; use radroots_sdk::trade::{ - RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRevisionDecision, + RadrootsActiveTradeFulfillmentState, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, + RadrootsTradeOrderRevisionDecision, }; use serde_json::json; @@ -1001,7 +999,7 @@ mod tests { listing_addr: "30402:seller:listing".to_owned(), buyer_pubkey: "buyer".to_owned(), seller_pubkey: "seller".to_owned(), - status: AppOrderFulfillmentPublishStatus::ReadyForPickup, + status: RadrootsActiveTradeFulfillmentState::ReadyForPickup, }); let receipt = AppPublishPayload::OrderReceipt(AppOrderReceiptPublishPayload { context: AppPublishContext::new("acct_local", "buyer_order_receipt"), @@ -1059,6 +1057,28 @@ mod tests { ); assert_eq!(receipt_reason_codes, vec!["unexpected_order_receipt_issue"]); + let invalid_fulfillment_reason_codes: Vec<&str> = + AppPublishPayload::OrderFulfillment(AppOrderFulfillmentPublishPayload { + context: AppPublishContext::new("acct_local", "seller_order_fulfillment"), + app_order_id: order_id, + farm_id, + trade_order_id: "order-1".to_owned(), + request_event_id: "request-event-1".to_owned(), + prev_event_id: "decision-event-1".to_owned(), + listing_addr: "30402:seller:listing".to_owned(), + buyer_pubkey: "buyer".to_owned(), + seller_pubkey: "seller".to_owned(), + status: RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled, + }) + .validation_failures() + .into_iter() + .map(AppPublishValidationFailure::storage_key) + .collect(); + assert_eq!( + invalid_fulfillment_reason_codes, + vec!["invalid_order_fulfillment_status"] + ); + let operation = PendingSyncOperation::from_publish_payload( AppPublishPayload::OrderFulfillment(AppOrderFulfillmentPublishPayload { context: AppPublishContext::new("acct_local", "seller_order_fulfillment"), @@ -1070,7 +1090,7 @@ mod tests { listing_addr: "30402:seller:listing".to_owned(), buyer_pubkey: "buyer".to_owned(), seller_pubkey: "seller".to_owned(), - status: AppOrderFulfillmentPublishStatus::Delivered, + status: RadrootsActiveTradeFulfillmentState::Delivered, }), "2026-04-20T18:00:00Z", ) @@ -1091,7 +1111,7 @@ mod tests { listing_addr: "30402:seller:listing".to_owned(), buyer_pubkey: "buyer".to_owned(), seller_pubkey: "seller".to_owned(), - status: AppOrderFulfillmentPublishStatus::Delivered, + status: RadrootsActiveTradeFulfillmentState::Delivered, }) ); }