commit 40ee2df06fe91816c8ef5a549c6694b44ab41015
parent 91e2ea30aa990841a87341ac61bc5c03621dd167
Author: triesap <tyson@radroots.org>
Date: Fri, 19 Jun 2026 16:02:23 -0700
runtime: expose SDK order status receipts
- render SDK order status evidence and eligibility metadata
- expose passive payment handoff and next action in status output
- keep legacy relay-derived status outside the SDK receipt shape
- guard and test the migrated status adapter metadata path
Diffstat:
4 files changed, 144 insertions(+), 4 deletions(-)
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -2266,6 +2266,7 @@ fn legacy_order_preflight_relay_status(
fulfillment: None,
lifecycle: None,
payment: None,
+ sdk_receipt: None,
reducer_issues: Vec::new(),
target_relays: Vec::new(),
connected_relays: Vec::new(),
@@ -2308,6 +2309,7 @@ fn legacy_order_preflight_relay_status(
fulfillment: None,
lifecycle: None,
payment: None,
+ sdk_receipt: None,
reducer_issues: Vec::new(),
target_relays,
connected_relays: Vec::new(),
@@ -2551,6 +2553,7 @@ fn order_status_reduction_from_receipt_inner(
fulfillment: None,
lifecycle: None,
payment: None,
+ sdk_receipt: None,
reducer_issues: vec![issue("order_id", message.clone())],
target_relays,
connected_relays,
@@ -2734,6 +2737,7 @@ fn order_status_reduction_from_receipt_inner(
fulfillment,
lifecycle: Some(lifecycle),
payment,
+ sdk_receipt: None,
reducer_issues,
target_relays,
connected_relays,
@@ -15596,6 +15600,7 @@ mod tests {
assert!(view.request_event_id.is_none());
assert!(view.economics.is_none());
assert!(view.fulfillment.is_none());
+ assert!(view.sdk_receipt.is_none());
assert!(view.reducer_issues.is_empty());
}
@@ -15676,6 +15681,24 @@ mod tests {
assert!(view.connected_relays.is_empty());
assert!(view.failed_relays.is_empty());
assert!(view.reducer_issues.is_empty());
+ let sdk_receipt = view.sdk_receipt.as_ref().expect("sdk receipt");
+ assert_eq!(
+ sdk_receipt.payment_handoff,
+ "in_person_or_off_platform_pending"
+ );
+ assert_eq!(
+ sdk_receipt.next_action,
+ "arrange_in_person_or_off_platform_payment"
+ );
+ assert_eq!(sdk_receipt.evidence.event_count, 2);
+ assert!(sdk_receipt.evidence.has_request);
+ assert!(sdk_receipt.evidence.has_decision);
+ assert!(sdk_receipt.evidence.has_agreement);
+ assert!(!sdk_receipt.evidence.has_issues);
+ assert!(!sdk_receipt.eligibility.can_decide);
+ assert!(sdk_receipt.eligibility.can_propose_revision);
+ assert!(sdk_receipt.eligibility.can_cancel);
+ assert!(sdk_receipt.eligibility.can_update_fulfillment);
let lifecycle = view.lifecycle.expect("lifecycle");
assert_eq!(lifecycle.phase, "accepted");
assert!(!lifecycle.terminal);
@@ -15825,6 +15848,12 @@ mod tests {
view.reducer_issues[0].event_ids,
vec![fork_event_id.to_string()]
);
+ let sdk_receipt = view.sdk_receipt.expect("sdk receipt");
+ assert_eq!(sdk_receipt.payment_handoff, "invalid");
+ assert_eq!(sdk_receipt.next_action, "inspect_evidence_issues");
+ assert!(sdk_receipt.evidence.has_issues);
+ assert!(!sdk_receipt.eligibility.can_decide);
+ assert!(!sdk_receipt.eligibility.can_update_fulfillment);
}
#[test]
diff --git a/src/runtime/order/sdk_status.rs b/src/runtime/order/sdk_status.rs
@@ -1,13 +1,15 @@
use radroots_events::ids::RadrootsEventId;
use radroots_sdk::{
- OrderFulfillmentStatusKind, OrderPaymentStateKind, OrderSettlementStateKind, OrderStatusKind,
- OrderStatusReceipt, SdkOrderStatusIssue,
+ OrderFulfillmentStatusKind, OrderPaymentHandoffKind, OrderPaymentStateKind,
+ OrderSettlementStateKind, OrderStatusEligibility, OrderStatusEvidenceSummary, OrderStatusKind,
+ OrderStatusNextActionKind, OrderStatusReceipt, SdkOrderStatusIssue,
};
use crate::view::runtime::{
- OrderIssueView, OrderStatusFulfillmentView, OrderStatusLifecycleCancellationView,
+ OrderIssueView, OrderStatusEligibilityView, OrderStatusEvidenceSummaryView,
+ OrderStatusFulfillmentView, OrderStatusLifecycleCancellationView,
OrderStatusLifecycleReceiptView, OrderStatusLifecycleView, OrderStatusPaymentView,
- OrderStatusView,
+ OrderStatusSdkReceiptView, OrderStatusView,
};
use super::{ORDER_ACTOR_CONTEXT_SDK_LOCAL, ORDER_STATUS_SDK_SOURCE};
@@ -26,6 +28,7 @@ pub(super) fn sdk_order_status_view(receipt: OrderStatusReceipt) -> OrderStatusV
&receipt,
reducer_issues.as_slice(),
));
+ let sdk_receipt = Some(sdk_order_status_receipt_view(&receipt));
OrderStatusView {
state,
@@ -46,6 +49,7 @@ pub(super) fn sdk_order_status_view(receipt: OrderStatusReceipt) -> OrderStatusV
fulfillment,
lifecycle: Some(lifecycle),
payment,
+ sdk_receipt,
reducer_issues,
target_relays: Vec::new(),
connected_relays: Vec::new(),
@@ -58,6 +62,78 @@ pub(super) fn sdk_order_status_view(receipt: OrderStatusReceipt) -> OrderStatusV
}
}
+fn sdk_order_status_receipt_view(receipt: &OrderStatusReceipt) -> OrderStatusSdkReceiptView {
+ OrderStatusSdkReceiptView {
+ payment_handoff: sdk_payment_handoff(receipt.payment_handoff).to_owned(),
+ next_action: sdk_status_next_action(receipt.next_action).to_owned(),
+ evidence: sdk_status_evidence_view(&receipt.evidence),
+ eligibility: sdk_status_eligibility_view(&receipt.eligibility),
+ }
+}
+
+fn sdk_status_evidence_view(
+ evidence: &OrderStatusEvidenceSummary,
+) -> OrderStatusEvidenceSummaryView {
+ OrderStatusEvidenceSummaryView {
+ event_count: evidence.event_count,
+ limit_applied: evidence.limit_applied,
+ has_request: evidence.has_request,
+ has_decision: evidence.has_decision,
+ has_agreement: evidence.has_agreement,
+ has_pending_revision: evidence.has_pending_revision,
+ has_fulfillment: evidence.has_fulfillment,
+ has_cancellation: evidence.has_cancellation,
+ has_receipt: evidence.has_receipt,
+ has_issues: evidence.has_issues,
+ }
+}
+
+fn sdk_status_eligibility_view(eligibility: &OrderStatusEligibility) -> OrderStatusEligibilityView {
+ OrderStatusEligibilityView {
+ can_decide: eligibility.can_decide,
+ can_propose_revision: eligibility.can_propose_revision,
+ can_decide_revision: eligibility.can_decide_revision,
+ can_cancel: eligibility.can_cancel,
+ can_update_fulfillment: eligibility.can_update_fulfillment,
+ can_record_receipt: eligibility.can_record_receipt,
+ }
+}
+
+fn sdk_payment_handoff(kind: OrderPaymentHandoffKind) -> &'static str {
+ match kind {
+ OrderPaymentHandoffKind::NotReady => "not_ready",
+ OrderPaymentHandoffKind::NotRequired => "not_required",
+ OrderPaymentHandoffKind::InPersonOrOffPlatformPending => {
+ "in_person_or_off_platform_pending"
+ }
+ OrderPaymentHandoffKind::InPersonOrOffPlatformRecorded => {
+ "in_person_or_off_platform_recorded"
+ }
+ OrderPaymentHandoffKind::InPersonOrOffPlatformSettled => {
+ "in_person_or_off_platform_settled"
+ }
+ OrderPaymentHandoffKind::Rejected => "rejected",
+ OrderPaymentHandoffKind::Invalid => "invalid",
+ _ => "unknown",
+ }
+}
+
+fn sdk_status_next_action(kind: OrderStatusNextActionKind) -> &'static str {
+ match kind {
+ OrderStatusNextActionKind::NoLocalOrder => "no_local_order",
+ OrderStatusNextActionKind::InspectEvidenceIssues => "inspect_evidence_issues",
+ OrderStatusNextActionKind::AwaitSellerDecision => "await_seller_decision",
+ OrderStatusNextActionKind::ArrangeInPersonOrOffPlatformPayment => {
+ "arrange_in_person_or_off_platform_payment"
+ }
+ OrderStatusNextActionKind::DecideRevision => "decide_revision",
+ OrderStatusNextActionKind::FulfillOrder => "fulfill_order",
+ OrderStatusNextActionKind::RecordReceipt => "record_receipt",
+ OrderStatusNextActionKind::Terminal => "terminal",
+ _ => "unknown",
+ }
+}
+
fn sdk_order_status_state(status: OrderStatusKind) -> &'static str {
match status {
OrderStatusKind::Missing => "missing",
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -455,6 +455,7 @@ mod tests {
"OrderStatusView",
"OrderStatusLifecycleView",
"OrderStatusPaymentView",
+ "OrderStatusSdkReceiptView",
],
},
MigratedCliPathGuard {
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -2415,6 +2415,8 @@ pub struct OrderStatusView {
pub lifecycle: Option<OrderStatusLifecycleView>,
#[serde(skip_serializing_if = "inactive_status_payment")]
pub payment: Option<OrderStatusPaymentView>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sdk_receipt: Option<OrderStatusSdkReceiptView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reducer_issues: Vec<OrderIssueView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -2436,6 +2438,38 @@ pub struct OrderStatusView {
}
#[derive(Debug, Clone, Serialize)]
+pub struct OrderStatusSdkReceiptView {
+ pub payment_handoff: String,
+ pub next_action: String,
+ pub evidence: OrderStatusEvidenceSummaryView,
+ pub eligibility: OrderStatusEligibilityView,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct OrderStatusEvidenceSummaryView {
+ pub event_count: usize,
+ pub limit_applied: u32,
+ pub has_request: bool,
+ pub has_decision: bool,
+ pub has_agreement: bool,
+ pub has_pending_revision: bool,
+ pub has_fulfillment: bool,
+ pub has_cancellation: bool,
+ pub has_receipt: bool,
+ pub has_issues: bool,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct OrderStatusEligibilityView {
+ pub can_decide: bool,
+ pub can_propose_revision: bool,
+ pub can_decide_revision: bool,
+ pub can_cancel: bool,
+ pub can_update_fulfillment: bool,
+ pub can_record_receipt: bool,
+}
+
+#[derive(Debug, Clone, Serialize)]
pub struct OrderStatusRevisionView {
pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]