cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

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:
Msrc/runtime/order.rs | 29+++++++++++++++++++++++++++++
Msrc/runtime/order/sdk_status.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/runtime/sdk.rs | 1+
Msrc/view/runtime.rs | 34++++++++++++++++++++++++++++++++++
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")]