commit 280ffcdcf3b5f301db4a5127f94855e6e6d93bf7
parent d620a3a81a2d8a4abbd0593140f572e97f9baeb2
Author: triesap <tyson@radroots.org>
Date: Wed, 3 Jun 2026 16:54:02 -0700
sync: use shared fulfillment state
Diffstat:
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,
})
);
}