commit 2dd31d71067eed4281d22bbfbaff5125223fd56f
parent 933b2de1308ab748607682924f1cda22fee327fd
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 17:12:34 -0700
sdk: enrich order status receipts
- Make order status requests carry typed order IDs with a parse constructor for user input.
- Expose local source, exact event count, applied limit, and typed event IDs on receipts.
- Replace issue counts with typed redacted reducer issues and event ID details.
- Expand runtime tests for missing, invalid, limited, accepted, malformed, and conflict states.
Diffstat:
4 files changed, 790 insertions(+), 49 deletions(-)
diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs
@@ -103,7 +103,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
let order_status = sdk
.orders()
- .status(OrderStatusRequest::new("example-order-1"))
+ .status(OrderStatusRequest::parse("example-order-1")?)
.await?;
assert_eq!(
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -61,7 +61,7 @@ pub use crate::listings_runtime::{
pub use crate::orders_runtime::{
ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, OrderFulfillmentStatusKind,
OrderPaymentStateKind, OrderSettlementStateKind, OrderStatusKind, OrderStatusReceipt,
- OrderStatusRequest,
+ OrderStatusRequest, SdkOrderStatusIssue, SdkOrderStatusIssueKind, SdkOrderStatusSource,
};
#[cfg(feature = "runtime")]
pub use crate::product_clients::{ListingsClient, OrdersClient, SyncClient};
diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs
@@ -1,11 +1,15 @@
#[cfg(feature = "runtime")]
use crate::{OrdersClient, RadrootsSdkError};
#[cfg(feature = "runtime")]
-use radroots_events::{ids::RadrootsOrderId, order::RadrootsOrderFulfillmentState};
+use radroots_events::{
+ ids::{RadrootsEventId, RadrootsOrderId},
+ order::RadrootsOrderFulfillmentState,
+};
#[cfg(feature = "runtime")]
use radroots_trade::order::{
- RadrootsOrderPaymentState, RadrootsOrderProjection, RadrootsOrderSettlementState,
- RadrootsOrderStatus, RadrootsOrderStoreQueryError, order_projection_for_order_id,
+ RadrootsOrderIssue, RadrootsOrderPaymentState, RadrootsOrderProjectionQueryResult,
+ RadrootsOrderSettlementState, RadrootsOrderStatus, RadrootsOrderStoreQueryError,
+ order_projection_query_for_order_id,
};
#[cfg(feature = "runtime")]
@@ -16,25 +20,33 @@ pub const ORDER_STATUS_MAX_LIMIT: u32 = 1_000;
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderStatusRequest {
- pub order_id: String,
+ pub order_id: RadrootsOrderId,
pub limit: u32,
}
#[cfg(feature = "runtime")]
impl OrderStatusRequest {
- pub fn new(order_id: impl Into<String>) -> Self {
+ pub fn new(order_id: RadrootsOrderId) -> Self {
Self {
- order_id: order_id.into(),
+ order_id,
limit: ORDER_STATUS_DEFAULT_LIMIT,
}
}
+ pub fn parse(order_id: &str) -> Result<Self, RadrootsSdkError> {
+ RadrootsOrderId::parse(order_id)
+ .map(Self::new)
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order_id is invalid: {error}"),
+ })
+ }
+
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = limit;
self
}
- fn validate(&self) -> Result<RadrootsOrderId, RadrootsSdkError> {
+ fn validate(&self) -> Result<(), RadrootsSdkError> {
if self.limit == 0 || self.limit > ORDER_STATUS_MAX_LIMIT {
return Err(RadrootsSdkError::InvalidRequest {
message: format!(
@@ -42,31 +54,37 @@ impl OrderStatusRequest {
),
});
}
- RadrootsOrderId::parse(self.order_id.as_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("order_id is invalid: {error}"),
- }
- })
+ Ok(())
}
}
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderStatusReceipt {
- pub order_id: String,
+ pub order_id: RadrootsOrderId,
+ pub source: SdkOrderStatusSource,
pub found: bool,
+ pub event_count: usize,
+ pub limit_applied: u32,
pub status: OrderStatusKind,
pub fulfillment_status: Option<OrderFulfillmentStatusKind>,
pub payment_state: OrderPaymentStateKind,
pub settlement_state: OrderSettlementStateKind,
pub lifecycle_terminal: bool,
- pub request_event_id: Option<String>,
- pub decision_event_id: Option<String>,
- pub fulfillment_event_id: Option<String>,
- pub cancellation_event_id: Option<String>,
- pub receipt_event_id: Option<String>,
- pub last_event_id: Option<String>,
- pub issue_count: usize,
+ pub event_ids: Vec<RadrootsEventId>,
+ pub request_event_id: Option<RadrootsEventId>,
+ pub decision_event_id: Option<RadrootsEventId>,
+ pub fulfillment_event_id: Option<RadrootsEventId>,
+ pub cancellation_event_id: Option<RadrootsEventId>,
+ pub receipt_event_id: Option<RadrootsEventId>,
+ pub last_event_id: Option<RadrootsEventId>,
+ pub issues: Vec<SdkOrderStatusIssue>,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SdkOrderStatusSource {
+ LocalEventStore,
}
#[cfg(feature = "runtime")]
@@ -114,39 +132,191 @@ pub enum OrderSettlementStateKind {
}
#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SdkOrderStatusIssue {
+ pub kind: SdkOrderStatusIssueKind,
+ pub event_ids: Vec<RadrootsEventId>,
+}
+
+#[cfg(feature = "runtime")]
+impl SdkOrderStatusIssue {
+ fn new(kind: SdkOrderStatusIssueKind, event_ids: Vec<RadrootsEventId>) -> Self {
+ Self { kind, event_ids }
+ }
+
+ fn single(kind: SdkOrderStatusIssueKind, event_id: RadrootsEventId) -> Self {
+ Self::new(kind, vec![event_id])
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SdkOrderStatusIssueKind {
+ MissingRequest,
+ MultipleRequests,
+ RequestPayloadInvalid,
+ RequestOrderIdMismatch,
+ RequestAuthorMismatch,
+ RequestListingAddressInvalid,
+ RequestSellerListingMismatch,
+ DecisionPayloadInvalid,
+ DecisionOrderIdMismatch,
+ DecisionAuthorMismatch,
+ DecisionCounterpartyMismatch,
+ DecisionBuyerMismatch,
+ DecisionSellerMismatch,
+ DecisionListingAddressInvalid,
+ DecisionListingMismatch,
+ DecisionRootMismatch,
+ DecisionPreviousMismatch,
+ DecisionMissingInventoryCommitments,
+ DecisionInventoryCommitmentMismatch,
+ DecisionMissingReason,
+ ConflictingDecisions,
+ RevisionProposalWithoutAcceptedDecision,
+ RevisionProposalPayloadInvalid,
+ RevisionProposalOrderIdMismatch,
+ RevisionProposalAuthorMismatch,
+ RevisionProposalCounterpartyMismatch,
+ RevisionProposalBuyerMismatch,
+ RevisionProposalSellerMismatch,
+ RevisionProposalListingAddressInvalid,
+ RevisionProposalListingMismatch,
+ RevisionProposalRootMismatch,
+ RevisionProposalPreviousMismatch,
+ RevisionDecisionWithoutProposal,
+ RevisionDecisionPayloadInvalid,
+ RevisionDecisionOrderIdMismatch,
+ RevisionDecisionAuthorMismatch,
+ RevisionDecisionCounterpartyMismatch,
+ RevisionDecisionBuyerMismatch,
+ RevisionDecisionSellerMismatch,
+ RevisionDecisionListingAddressInvalid,
+ RevisionDecisionListingMismatch,
+ RevisionDecisionRootMismatch,
+ RevisionDecisionPreviousMismatch,
+ RevisionDecisionRevisionIdMismatch,
+ FulfillmentWithoutAcceptedDecision,
+ FulfillmentPayloadInvalid,
+ FulfillmentOrderIdMismatch,
+ FulfillmentAuthorMismatch,
+ FulfillmentCounterpartyMismatch,
+ FulfillmentBuyerMismatch,
+ FulfillmentSellerMismatch,
+ FulfillmentListingAddressInvalid,
+ FulfillmentListingMismatch,
+ FulfillmentRootMismatch,
+ FulfillmentPreviousMismatch,
+ FulfillmentStatusNotPublishable,
+ FulfillmentUnsupportedTransition,
+ ForkedFulfillments,
+ CancellationWithoutCancellableOrder,
+ CancellationPayloadInvalid,
+ CancellationOrderIdMismatch,
+ CancellationAuthorMismatch,
+ CancellationCounterpartyMismatch,
+ CancellationBuyerMismatch,
+ CancellationSellerMismatch,
+ CancellationListingAddressInvalid,
+ CancellationListingMismatch,
+ CancellationRootMismatch,
+ CancellationPreviousMismatch,
+ CancellationAfterFulfillment,
+ ReceiptWithoutEligibleFulfillment,
+ ReceiptPayloadInvalid,
+ ReceiptOrderIdMismatch,
+ ReceiptAuthorMismatch,
+ ReceiptCounterpartyMismatch,
+ ReceiptBuyerMismatch,
+ ReceiptSellerMismatch,
+ ReceiptListingAddressInvalid,
+ ReceiptListingMismatch,
+ ReceiptRootMismatch,
+ ReceiptPreviousMismatch,
+ PaymentWithoutAcceptedAgreement,
+ PaymentPayloadInvalid,
+ PaymentOrderIdMismatch,
+ PaymentAuthorMismatch,
+ PaymentCounterpartyMismatch,
+ PaymentBuyerMismatch,
+ PaymentSellerMismatch,
+ PaymentListingAddressInvalid,
+ PaymentListingMismatch,
+ PaymentRootMismatch,
+ PaymentPreviousMismatch,
+ PaymentAgreementMismatch,
+ PaymentQuoteMismatch,
+ PaymentQuoteVersionMismatch,
+ PaymentEconomicsDigestMismatch,
+ PaymentAmountMismatch,
+ PaymentCurrencyMismatch,
+ PaymentAfterCancellation,
+ RevisionAfterPayment,
+ DuplicatePayments,
+ SettlementWithoutValidPayment,
+ SettlementPayloadInvalid,
+ SettlementOrderIdMismatch,
+ SettlementAuthorMismatch,
+ SettlementCounterpartyMismatch,
+ SettlementBuyerMismatch,
+ SettlementSellerMismatch,
+ SettlementListingAddressInvalid,
+ SettlementListingMismatch,
+ SettlementRootMismatch,
+ SettlementPreviousMismatch,
+ SettlementPaymentEventMismatch,
+ SettlementAgreementMismatch,
+ SettlementQuoteMismatch,
+ SettlementQuoteVersionMismatch,
+ SettlementEconomicsDigestMismatch,
+ SettlementAmountMismatch,
+ SettlementCurrencyMismatch,
+ DuplicateSettlements,
+ ForkedLifecycle,
+}
+
+#[cfg(feature = "runtime")]
impl<'sdk> OrdersClient<'sdk> {
pub async fn status(
&self,
request: OrderStatusRequest,
) -> Result<OrderStatusReceipt, RadrootsSdkError> {
- let order_id = request.validate()?;
- let projection =
- order_projection_for_order_id(&self.sdk._event_store, &order_id, request.limit)
- .await
- .map_err(projection_error)?;
- Ok(OrderStatusReceipt::from_projection(projection))
+ request.validate()?;
+ let query_result = order_projection_query_for_order_id(
+ &self.sdk._event_store,
+ &request.order_id,
+ request.limit,
+ )
+ .await
+ .map_err(projection_error)?;
+ Ok(OrderStatusReceipt::from_query_result(query_result))
}
}
#[cfg(feature = "runtime")]
impl OrderStatusReceipt {
- fn from_projection(projection: RadrootsOrderProjection) -> Self {
+ fn from_query_result(query_result: RadrootsOrderProjectionQueryResult) -> Self {
+ let projection = query_result.projection;
let found = projection.status != RadrootsOrderStatus::Missing;
Self {
- order_id: projection.order_id.into_string(),
+ order_id: projection.order_id,
+ source: SdkOrderStatusSource::LocalEventStore,
found,
+ event_count: query_result.event_count,
+ limit_applied: query_result.limit_applied,
status: projection.status.into(),
fulfillment_status: projection.fulfillment_status.map(Into::into),
payment_state: projection.payment.state.into(),
settlement_state: projection.payment.settlement_state.into(),
lifecycle_terminal: projection.lifecycle_terminal,
- request_event_id: projection.request_event_id.map(Into::into),
- decision_event_id: projection.decision_event_id.map(Into::into),
- fulfillment_event_id: projection.fulfillment_event_id.map(Into::into),
- cancellation_event_id: projection.cancellation_event_id.map(Into::into),
- receipt_event_id: projection.receipt_event_id.map(Into::into),
- last_event_id: projection.last_event_id.map(Into::into),
- issue_count: projection.issues.len(),
+ event_ids: query_result.event_ids,
+ request_event_id: projection.request_event_id,
+ decision_event_id: projection.decision_event_id,
+ fulfillment_event_id: projection.fulfillment_event_id,
+ cancellation_event_id: projection.cancellation_event_id,
+ receipt_event_id: projection.receipt_event_id,
+ last_event_id: projection.last_event_id,
+ issues: projection.issues.into_iter().map(Into::into).collect(),
}
}
}
@@ -208,6 +378,443 @@ impl From<RadrootsOrderSettlementState> for OrderSettlementStateKind {
}
#[cfg(feature = "runtime")]
+impl From<RadrootsOrderIssue> for SdkOrderStatusIssue {
+ fn from(issue: RadrootsOrderIssue) -> Self {
+ match issue {
+ RadrootsOrderIssue::MissingRequest => {
+ Self::new(SdkOrderStatusIssueKind::MissingRequest, Vec::new())
+ }
+ RadrootsOrderIssue::MultipleRequests { event_ids } => {
+ Self::new(SdkOrderStatusIssueKind::MultipleRequests, event_ids)
+ }
+ RadrootsOrderIssue::RequestPayloadInvalid { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::RequestPayloadInvalid, event_id)
+ }
+ RadrootsOrderIssue::RequestOrderIdMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::RequestOrderIdMismatch, event_id)
+ }
+ RadrootsOrderIssue::RequestAuthorMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::RequestAuthorMismatch, event_id)
+ }
+ RadrootsOrderIssue::RequestListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RequestListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::RequestSellerListingMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RequestSellerListingMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::DecisionPayloadInvalid { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionPayloadInvalid, event_id)
+ }
+ RadrootsOrderIssue::DecisionOrderIdMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionOrderIdMismatch, event_id)
+ }
+ RadrootsOrderIssue::DecisionAuthorMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionAuthorMismatch, event_id)
+ }
+ RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::DecisionCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::DecisionBuyerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionBuyerMismatch, event_id)
+ }
+ RadrootsOrderIssue::DecisionSellerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionSellerMismatch, event_id)
+ }
+ RadrootsOrderIssue::DecisionListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::DecisionListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::DecisionListingMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionListingMismatch, event_id)
+ }
+ RadrootsOrderIssue::DecisionRootMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionRootMismatch, event_id)
+ }
+ RadrootsOrderIssue::DecisionPreviousMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionPreviousMismatch, event_id)
+ }
+ RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id } => Self::single(
+ SdkOrderStatusIssueKind::DecisionMissingInventoryCommitments,
+ event_id,
+ ),
+ RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::DecisionInventoryCommitmentMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::DecisionMissingReason { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::DecisionMissingReason, event_id)
+ }
+ RadrootsOrderIssue::ConflictingDecisions { event_ids } => {
+ Self::new(SdkOrderStatusIssueKind::ConflictingDecisions, event_ids)
+ }
+ RadrootsOrderIssue::RevisionProposalWithoutAcceptedDecision { event_id } => {
+ Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalWithoutAcceptedDecision,
+ event_id,
+ )
+ }
+ RadrootsOrderIssue::RevisionProposalPayloadInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalPayloadInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalOrderIdMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalOrderIdMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalAuthorMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalAuthorMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalBuyerMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalBuyerMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalSellerMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalSellerMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalListingMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalListingMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalRootMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalRootMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionProposalPreviousMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionProposalPreviousMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionWithoutProposal,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionPayloadInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionPayloadInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionOrderIdMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionAuthorMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionBuyerMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionBuyerMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionSellerMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionSellerMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionListingMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionListingMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionRootMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionRootMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionPreviousMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionPreviousMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::RevisionDecisionRevisionIdMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentWithoutAcceptedDecision,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentPayloadInvalid { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::FulfillmentPayloadInvalid, event_id)
+ }
+ RadrootsOrderIssue::FulfillmentOrderIdMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentOrderIdMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentAuthorMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::FulfillmentAuthorMismatch, event_id)
+ }
+ RadrootsOrderIssue::FulfillmentCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentBuyerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::FulfillmentBuyerMismatch, event_id)
+ }
+ RadrootsOrderIssue::FulfillmentSellerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::FulfillmentSellerMismatch, event_id)
+ }
+ RadrootsOrderIssue::FulfillmentListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentListingMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentListingMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentRootMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::FulfillmentRootMismatch, event_id)
+ }
+ RadrootsOrderIssue::FulfillmentPreviousMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentPreviousMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentStatusNotPublishable { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentStatusNotPublishable,
+ event_id,
+ ),
+ RadrootsOrderIssue::FulfillmentUnsupportedTransition { event_id } => Self::single(
+ SdkOrderStatusIssueKind::FulfillmentUnsupportedTransition,
+ event_id,
+ ),
+ RadrootsOrderIssue::ForkedFulfillments { event_ids } => {
+ Self::new(SdkOrderStatusIssueKind::ForkedFulfillments, event_ids)
+ }
+ RadrootsOrderIssue::CancellationWithoutCancellableOrder { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationWithoutCancellableOrder,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationPayloadInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationPayloadInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationOrderIdMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationOrderIdMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationAuthorMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationAuthorMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationBuyerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::CancellationBuyerMismatch, event_id)
+ }
+ RadrootsOrderIssue::CancellationSellerMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationSellerMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationListingMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationListingMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationRootMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::CancellationRootMismatch, event_id)
+ }
+ RadrootsOrderIssue::CancellationPreviousMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationPreviousMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::CancellationAfterFulfillment { event_id } => Self::single(
+ SdkOrderStatusIssueKind::CancellationAfterFulfillment,
+ event_id,
+ ),
+ RadrootsOrderIssue::ReceiptWithoutEligibleFulfillment { event_id } => Self::single(
+ SdkOrderStatusIssueKind::ReceiptWithoutEligibleFulfillment,
+ event_id,
+ ),
+ RadrootsOrderIssue::ReceiptPayloadInvalid { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptPayloadInvalid, event_id)
+ }
+ RadrootsOrderIssue::ReceiptOrderIdMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptOrderIdMismatch, event_id)
+ }
+ RadrootsOrderIssue::ReceiptAuthorMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptAuthorMismatch, event_id)
+ }
+ RadrootsOrderIssue::ReceiptCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::ReceiptCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::ReceiptBuyerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptBuyerMismatch, event_id)
+ }
+ RadrootsOrderIssue::ReceiptSellerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptSellerMismatch, event_id)
+ }
+ RadrootsOrderIssue::ReceiptListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::ReceiptListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::ReceiptListingMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptListingMismatch, event_id)
+ }
+ RadrootsOrderIssue::ReceiptRootMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptRootMismatch, event_id)
+ }
+ RadrootsOrderIssue::ReceiptPreviousMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::ReceiptPreviousMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentWithoutAcceptedAgreement { event_id } => Self::single(
+ SdkOrderStatusIssueKind::PaymentWithoutAcceptedAgreement,
+ event_id,
+ ),
+ RadrootsOrderIssue::PaymentPayloadInvalid { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentPayloadInvalid, event_id)
+ }
+ RadrootsOrderIssue::PaymentOrderIdMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentOrderIdMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentAuthorMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentAuthorMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::PaymentCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::PaymentBuyerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentBuyerMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentSellerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentSellerMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::PaymentListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::PaymentListingMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentListingMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentRootMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentRootMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentPreviousMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentPreviousMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentAgreementMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentAgreementMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentQuoteMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentQuoteMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentQuoteVersionMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::PaymentQuoteVersionMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::PaymentEconomicsDigestMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::PaymentEconomicsDigestMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::PaymentAmountMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentAmountMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentCurrencyMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentCurrencyMismatch, event_id)
+ }
+ RadrootsOrderIssue::PaymentAfterCancellation { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::PaymentAfterCancellation, event_id)
+ }
+ RadrootsOrderIssue::RevisionAfterPayment { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::RevisionAfterPayment, event_id)
+ }
+ RadrootsOrderIssue::DuplicatePayments { event_ids } => {
+ Self::new(SdkOrderStatusIssueKind::DuplicatePayments, event_ids)
+ }
+ RadrootsOrderIssue::SettlementWithoutValidPayment { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementWithoutValidPayment,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementPayloadInvalid { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementPayloadInvalid, event_id)
+ }
+ RadrootsOrderIssue::SettlementOrderIdMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementOrderIdMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementAuthorMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementAuthorMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementCounterpartyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementCounterpartyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementBuyerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementBuyerMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementSellerMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementSellerMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementListingAddressInvalid { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementListingAddressInvalid,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementListingMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementListingMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementRootMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementRootMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementPreviousMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementPreviousMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementPaymentEventMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementPaymentEventMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementAgreementMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementAgreementMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementQuoteMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementQuoteMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementQuoteVersionMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementQuoteVersionMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementEconomicsDigestMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementEconomicsDigestMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::SettlementAmountMismatch { event_id } => {
+ Self::single(SdkOrderStatusIssueKind::SettlementAmountMismatch, event_id)
+ }
+ RadrootsOrderIssue::SettlementCurrencyMismatch { event_id } => Self::single(
+ SdkOrderStatusIssueKind::SettlementCurrencyMismatch,
+ event_id,
+ ),
+ RadrootsOrderIssue::DuplicateSettlements { event_ids } => {
+ Self::new(SdkOrderStatusIssueKind::DuplicateSettlements, event_ids)
+ }
+ RadrootsOrderIssue::ForkedLifecycle { event_ids } => {
+ Self::new(SdkOrderStatusIssueKind::ForkedLifecycle, event_ids)
+ }
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
fn projection_error(error: RadrootsOrderStoreQueryError) -> RadrootsSdkError {
let message = match error {
RadrootsOrderStoreQueryError::Store(_) => "order status store query failed",
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -24,7 +24,7 @@ use radroots_sdk::protocol::order::{
use radroots_sdk::{
ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, OrderPaymentStateKind,
OrderSettlementStateKind, OrderStatusKind, OrderStatusRequest, RadrootsSdk, RadrootsSdkError,
- RadrootsSdkTimestamp,
+ RadrootsSdkTimestamp, SdkOrderStatusIssueKind, SdkOrderStatusSource,
};
const BUYER_SECRET_KEY_HEX: &str =
@@ -55,6 +55,10 @@ fn order_id(raw: &str) -> RadrootsOrderId {
RadrootsOrderId::parse(raw).expect("order id")
}
+fn status_request(raw: &str) -> OrderStatusRequest {
+ OrderStatusRequest::parse(raw).expect("order status request")
+}
+
fn listing_address() -> RadrootsListingAddress {
RadrootsListingAddress::parse(format!(
"{KIND_LISTING}:{SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg"
@@ -189,21 +193,25 @@ fn signed_order_decision_event(
#[tokio::test]
async fn order_status_returns_not_found_for_missing_local_order() {
let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let request = OrderStatusRequest::new("order-1");
+ let request = status_request("order-1");
assert_eq!(request.limit, ORDER_STATUS_DEFAULT_LIMIT);
let receipt = sdk.orders().status(request).await.expect("status");
assert!(!receipt.found);
- assert_eq!(receipt.order_id, "order-1");
+ assert_eq!(receipt.order_id.as_str(), "order-1");
+ assert_eq!(receipt.source, SdkOrderStatusSource::LocalEventStore);
+ assert_eq!(receipt.event_count, 0);
+ assert_eq!(receipt.limit_applied, ORDER_STATUS_DEFAULT_LIMIT);
+ assert!(receipt.event_ids.is_empty());
assert_eq!(receipt.status, OrderStatusKind::Missing);
assert_eq!(receipt.payment_state, OrderPaymentStateKind::NotRecorded);
assert_eq!(
receipt.settlement_state,
OrderSettlementStateKind::NotRequired
);
- assert_eq!(receipt.issue_count, 0);
+ assert!(receipt.issues.is_empty());
}
#[tokio::test]
@@ -212,12 +220,12 @@ async fn order_status_rejects_invalid_limits_before_querying() {
let zero = sdk
.orders()
- .status(OrderStatusRequest::new("order-1").with_limit(0))
+ .status(status_request("order-1").with_limit(0))
.await
.expect_err("zero limit");
let too_large = sdk
.orders()
- .status(OrderStatusRequest::new("order-1").with_limit(ORDER_STATUS_MAX_LIMIT + 1))
+ .status(status_request("order-1").with_limit(ORDER_STATUS_MAX_LIMIT + 1))
.await
.expect_err("too large");
@@ -225,6 +233,13 @@ async fn order_status_rejects_invalid_limits_before_querying() {
assert!(matches!(too_large, RadrootsSdkError::InvalidRequest { .. }));
}
+#[test]
+fn order_status_parse_rejects_invalid_order_ids() {
+ let error = OrderStatusRequest::parse("bad order id").expect_err("invalid order id");
+
+ assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
+}
+
#[tokio::test]
async fn order_status_projects_local_request_and_decision_events() {
let (_tempdir, sdk, store) = directory_sdk_and_store().await;
@@ -244,29 +259,148 @@ async fn order_status_projects_local_request_and_decision_events() {
let receipt = sdk
.orders()
- .status(OrderStatusRequest::new("order-1").with_limit(1_000))
+ .status(status_request("order-1").with_limit(1_000))
.await
.expect("status");
assert!(receipt.found);
+ assert_eq!(receipt.order_id.as_str(), "order-1");
+ assert_eq!(receipt.source, SdkOrderStatusSource::LocalEventStore);
+ assert_eq!(receipt.event_count, 2);
+ assert_eq!(receipt.limit_applied, 1_000);
+ assert_eq!(
+ receipt
+ .event_ids
+ .iter()
+ .map(RadrootsEventId::as_str)
+ .collect::<Vec<_>>(),
+ vec![request_event.id.as_str(), decision_event.id.as_str()]
+ );
assert_eq!(receipt.status, OrderStatusKind::Accepted);
assert_eq!(
- receipt.request_event_id.as_deref(),
+ receipt
+ .request_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
Some(request_event.id.as_str())
);
assert_eq!(
- receipt.decision_event_id.as_deref(),
+ receipt
+ .decision_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
Some(decision_event.id.as_str())
);
assert_eq!(
- receipt.last_event_id.as_deref(),
+ receipt.last_event_id.as_ref().map(RadrootsEventId::as_str),
Some(decision_event.id.as_str())
);
- assert_eq!(receipt.issue_count, 0);
+ assert!(receipt.issues.is_empty());
assert!(!receipt.lifecycle_terminal);
}
#[tokio::test]
+async fn order_status_reports_limited_local_results() {
+ let (_tempdir, sdk, store) = directory_sdk_and_store().await;
+ let request_event = signed_order_request_event("order-1", 25);
+ let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id");
+ let decision_event = signed_order_decision_event("order-1", &request_event_id, 26);
+
+ for (event, observed_at_ms) in [(request_event.clone(), 2_500), (decision_event, 2_600)] {
+ store
+ .ingest_event(RadrootsEventIngest::new(event, observed_at_ms))
+ .await
+ .expect("ingest");
+ }
+
+ let receipt = sdk
+ .orders()
+ .status(status_request("order-1").with_limit(1))
+ .await
+ .expect("status");
+
+ assert!(receipt.found);
+ assert_eq!(receipt.status, OrderStatusKind::Requested);
+ assert_eq!(receipt.event_count, 1);
+ assert_eq!(receipt.limit_applied, 1);
+ assert_eq!(
+ receipt
+ .event_ids
+ .iter()
+ .map(RadrootsEventId::as_str)
+ .collect::<Vec<_>>(),
+ vec![request_event.id.as_str()]
+ );
+ assert_eq!(
+ receipt
+ .request_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
+ Some(request_event.id.as_str())
+ );
+ assert!(receipt.decision_event_id.is_none());
+ assert_eq!(
+ receipt.last_event_id.as_ref().map(RadrootsEventId::as_str),
+ Some(request_event.id.as_str())
+ );
+ assert!(receipt.issues.is_empty());
+}
+
+#[tokio::test]
+async fn order_status_reports_typed_reducer_issues() {
+ let (_tempdir, sdk, store) = directory_sdk_and_store().await;
+ let first_request_event = signed_order_request_event("order-1", 27);
+ let second_request_event = signed_order_request_event("order-1", 28);
+
+ for (event, observed_at_ms) in [
+ (first_request_event.clone(), 2_700),
+ (second_request_event.clone(), 2_800),
+ ] {
+ store
+ .ingest_event(RadrootsEventIngest::new(event, observed_at_ms))
+ .await
+ .expect("ingest");
+ }
+
+ let receipt = sdk
+ .orders()
+ .status(status_request("order-1"))
+ .await
+ .expect("status");
+
+ assert!(receipt.found);
+ assert_eq!(receipt.status, OrderStatusKind::Invalid);
+ assert_eq!(receipt.event_count, 2);
+ assert_eq!(
+ receipt
+ .event_ids
+ .iter()
+ .map(RadrootsEventId::as_str)
+ .collect::<Vec<_>>(),
+ vec![
+ first_request_event.id.as_str(),
+ second_request_event.id.as_str()
+ ]
+ );
+ let issue = receipt
+ .issues
+ .iter()
+ .find(|issue| issue.kind == SdkOrderStatusIssueKind::MultipleRequests)
+ .expect("multiple request issue");
+ assert_eq!(
+ issue
+ .event_ids
+ .iter()
+ .map(RadrootsEventId::as_str)
+ .collect::<Vec<_>>(),
+ vec![
+ first_request_event.id.as_str(),
+ second_request_event.id.as_str()
+ ]
+ );
+}
+
+#[tokio::test]
async fn order_status_maps_malformed_local_data_to_sanitized_error() {
let (_tempdir, sdk, store) = directory_sdk_and_store().await;
let request_event = signed_order_request_event("order-1", 30);
@@ -283,7 +417,7 @@ async fn order_status_maps_malformed_local_data_to_sanitized_error() {
let error = sdk
.orders()
- .status(OrderStatusRequest::new("order-1"))
+ .status(status_request("order-1"))
.await
.expect_err("projection error");
let message = error.to_string();