sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit c4ce4ecda87c69a6d9fb2479300501c9c589e7aa
parent d1c1b9c030e1f6168d1d9fb09c3d5dac49437b5a
Author: triesap <tyson@radroots.org>
Date:   Fri, 19 Jun 2026 18:17:25 -0700

orders: slim SDK order runtime API

- remove post-agreement order workflow methods and exports
- expose agreement evidence on order status receipts
- align order lifecycle preflight with requested negotiation state
- update runtime and boundary tests for agreement-only status

Diffstat:
Mcrates/sdk/src/lib.rs | 19+++++++------------
Mcrates/sdk/src/order.rs | 75---------------------------------------------------------------------------
Mcrates/sdk/src/orders_runtime.rs | 1819++++++++++++-------------------------------------------------------------------
Mcrates/sdk/tests/orders_runtime.rs | 423+++++++++++++++++++------------------------------------------------------------
Mcrates/sdk/tests/source_boundary.rs | 77+++++++++++++++++++++++++++++++++--------------------------------------------
5 files changed, 420 insertions(+), 1993 deletions(-)

diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -61,26 +61,21 @@ pub use crate::listings_runtime::{ #[cfg(feature = "runtime")] pub use crate::orders_runtime::{ ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, - ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, ORDER_SUBMIT_OPERATION_KIND, OrderCancellationEnqueueRequest, OrderCancellationPlan, OrderCancellationPrepareRequest, OrderCancellationReceipt, OrderDecisionEnqueueRequest, OrderDecisionPlan, OrderDecisionPrepareRequest, OrderDecisionReceipt, OrderEvidenceIngestReceipt, - OrderEvidenceIngestRequest, OrderFulfillmentStatusKind, OrderFulfillmentUpdateEnqueueRequest, - OrderFulfillmentUpdatePlan, OrderFulfillmentUpdatePrepareRequest, - OrderFulfillmentUpdateReceipt, OrderPaymentHandoffKind, OrderPaymentStateKind, - OrderReceiptRecordEnqueueRequest, OrderReceiptRecordPlan, OrderReceiptRecordPrepareRequest, - OrderReceiptRecordReceipt, OrderRequestEvidenceIngestReceipt, + OrderEvidenceIngestRequest, OrderRequestEvidenceIngestReceipt, OrderRequestEvidenceIngestRequest, OrderRevisionDecisionEnqueueRequest, OrderRevisionDecisionPlan, OrderRevisionDecisionPrepareRequest, OrderRevisionDecisionReceipt, OrderRevisionProposalEnqueueRequest, OrderRevisionProposalPlan, - OrderRevisionProposalPrepareRequest, OrderRevisionProposalReceipt, OrderSettlementStateKind, - OrderStatusEligibility, OrderStatusEvidenceSummary, OrderStatusKind, OrderStatusNextActionKind, - OrderStatusReceipt, OrderStatusRequest, OrderSubmitEnqueueRequest, OrderSubmitPlan, - OrderSubmitPrepareRequest, OrderSubmitReceipt, OrderWorkflowEnqueueReceipt, - OrderWorkflowIdempotencyReceipt, OrderWorkflowKind, OrderWorkflowPlan, - OrderWorkflowRetryAdvice, SdkOrderStatusIssue, SdkOrderStatusIssueKind, SdkOrderStatusSource, + OrderRevisionProposalPrepareRequest, OrderRevisionProposalReceipt, OrderStatusEligibility, + OrderStatusEvidenceSummary, OrderStatusKind, OrderStatusNextActionKind, OrderStatusReceipt, + OrderStatusRequest, OrderSubmitEnqueueRequest, OrderSubmitPlan, OrderSubmitPrepareRequest, + OrderSubmitReceipt, OrderWorkflowEnqueueReceipt, OrderWorkflowIdempotencyReceipt, + OrderWorkflowKind, OrderWorkflowPlan, OrderWorkflowRetryAdvice, SdkOrderStatusIssue, + SdkOrderStatusIssueKind, SdkOrderStatusSource, }; #[cfg(feature = "runtime")] pub use crate::product_clients::{FarmsClient, ListingsClient, OrdersClient, SyncClient}; diff --git a/crates/sdk/src/order.rs b/crates/sdk/src/order.rs @@ -33,20 +33,10 @@ pub struct RadrootsOrderRevisionDecisionDraft { } #[derive(Debug, Clone)] -pub struct RadrootsOrderFulfillmentUpdateDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] pub struct RadrootsOrderCancellationDraft { parts: WireEventParts, } -#[derive(Debug, Clone)] -pub struct RadrootsOrderReceiptDraft { - parts: WireEventParts, -} - impl RadrootsOrderRequestDraft { pub fn as_wire_parts(&self) -> &WireEventParts { &self.parts @@ -87,16 +77,6 @@ impl RadrootsOrderRevisionDecisionDraft { } } -impl RadrootsOrderFulfillmentUpdateDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - impl RadrootsOrderCancellationDraft { pub fn as_wire_parts(&self) -> &WireEventParts { &self.parts @@ -107,16 +87,6 @@ impl RadrootsOrderCancellationDraft { } } -impl RadrootsOrderReceiptDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - #[cfg(feature = "serde_json")] pub fn build_order_request_draft( listing_event: &RadrootsNostrEventPtr, @@ -173,21 +143,6 @@ pub fn build_order_revision_decision_draft( } #[cfg(feature = "serde_json")] -pub fn build_fulfillment_update_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderFulfillmentUpdate, -) -> Result<RadrootsOrderFulfillmentUpdateDraft, EventEncodeError> { - Ok(RadrootsOrderFulfillmentUpdateDraft { - parts: radroots_events_codec::order::order_fulfillment_update_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] pub fn build_order_cancellation_draft( root_event_id: &RadrootsEventId, prev_event_id: &RadrootsEventId, @@ -203,21 +158,6 @@ pub fn build_order_cancellation_draft( } #[cfg(feature = "serde_json")] -pub fn build_buyer_receipt_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderReceipt, -) -> Result<RadrootsOrderReceiptDraft, EventEncodeError> { - Ok(RadrootsOrderReceiptDraft { - parts: radroots_events_codec::order::order_receipt_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] pub fn parse_order_request( event: &RadrootsNostrEvent, ) -> Result<RadrootsOrderEnvelope<RadrootsOrderRequest>, RadrootsOrderEnvelopeParseError> { @@ -246,14 +186,6 @@ pub fn parse_order_revision_decision( } #[cfg(feature = "serde_json")] -pub fn parse_fulfillment_update( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderFulfillmentUpdate>, RadrootsOrderEnvelopeParseError> -{ - radroots_events_codec::order::order_fulfillment_update_from_event(event) -} - -#[cfg(feature = "serde_json")] pub fn parse_order_cancellation( event: &RadrootsNostrEvent, ) -> Result<RadrootsOrderEnvelope<RadrootsOrderCancellation>, RadrootsOrderEnvelopeParseError> { @@ -261,13 +193,6 @@ pub fn parse_order_cancellation( } #[cfg(feature = "serde_json")] -pub fn parse_buyer_receipt( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderReceipt>, RadrootsOrderEnvelopeParseError> { - radroots_events_codec::order::order_receipt_from_event(event) -} - -#[cfg(feature = "serde_json")] pub fn parse_listing_address( listing_addr: &str, ) -> Result<RadrootsListingAddress, RadrootsIdParseError> { diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs @@ -17,31 +17,27 @@ use radroots_events::{ draft::RadrootsFrozenEventDraft, ids::{RadrootsEventId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey}, kinds::{ - KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_FULFILLMENT_UPDATE, - KIND_ORDER_PAYMENT_RECORD, KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST, - KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_ORDER_SETTLEMENT_DECISION, + KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, + KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, }, order::{ - RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderFulfillmentState, - RadrootsOrderFulfillmentUpdate, RadrootsOrderReceipt, RadrootsOrderRequest, - RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal, + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderEconomics, + RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal, }, }; #[cfg(feature = "runtime")] use radroots_events_codec::order::{ - order_cancellation_from_event, order_decision_from_event, order_fulfillment_update_from_event, - order_payment_record_from_event, order_receipt_from_event, order_request_from_event, + order_cancellation_from_event, order_decision_from_event, order_request_from_event, order_revision_decision_from_event, order_revision_proposal_from_event, - order_settlement_decision_from_event, }; #[cfg(feature = "runtime")] use radroots_events_codec::wire::{WireEventParts, to_frozen_draft}; #[cfg(feature = "runtime")] use radroots_trade::order::{ - RadrootsOrderCanonicalizationError, RadrootsOrderIssue, RadrootsOrderPaymentState, - RadrootsOrderProjection, RadrootsOrderProjectionQueryResult, RadrootsOrderSettlementState, - RadrootsOrderStatus, RadrootsOrderStoreQueryError, canonicalize_order_decision_for_signer, - canonicalize_order_request_for_signer, order_projection_query_for_order_id, + RadrootsOrderCanonicalizationError, RadrootsOrderIssue, RadrootsOrderProjection, + RadrootsOrderProjectionQueryResult, RadrootsOrderStatus, RadrootsOrderStoreQueryError, + canonicalize_order_decision_for_signer, canonicalize_order_request_for_signer, + order_projection_query_for_order_id, }; #[cfg(feature = "runtime")] use serde::ser::SerializeStruct; @@ -60,10 +56,6 @@ pub const ORDER_REVISION_PROPOSAL_OPERATION_KIND: &str = "order.revision.proposa pub const ORDER_REVISION_DECISION_OPERATION_KIND: &str = "order.revision.decision.v1"; #[cfg(feature = "runtime")] pub const ORDER_CANCELLATION_OPERATION_KIND: &str = "order.cancellation.v1"; -#[cfg(feature = "runtime")] -pub const ORDER_FULFILLMENT_UPDATE_OPERATION_KIND: &str = "order.fulfillment.update.v1"; -#[cfg(feature = "runtime")] -pub const ORDER_RECEIPT_RECORD_OPERATION_KIND: &str = "order.receipt.record.v1"; #[cfg(feature = "runtime")] const ORDER_REQUEST_CONTRACT_ID: &str = "radroots.order.request.v1"; @@ -75,10 +67,6 @@ const ORDER_REVISION_PROPOSAL_CONTRACT_ID: &str = "radroots.order.revision_propo const ORDER_REVISION_DECISION_CONTRACT_ID: &str = "radroots.order.revision_decision.v1"; #[cfg(feature = "runtime")] const ORDER_CANCELLATION_CONTRACT_ID: &str = "radroots.order.cancellation.v1"; -#[cfg(feature = "runtime")] -const ORDER_FULFILLMENT_UPDATE_CONTRACT_ID: &str = "radroots.order.fulfillment_update.v1"; -#[cfg(feature = "runtime")] -const ORDER_RECEIPT_CONTRACT_ID: &str = "radroots.order.receipt.v1"; #[cfg(feature = "runtime")] #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] @@ -90,8 +78,6 @@ pub enum OrderWorkflowKind { RevisionProposal, RevisionDecision, Cancellation, - FulfillmentUpdate, - ReceiptRecord, } #[cfg(feature = "runtime")] @@ -103,8 +89,6 @@ impl OrderWorkflowKind { Self::RevisionProposal => ORDER_REVISION_PROPOSAL_OPERATION_KIND, Self::RevisionDecision => ORDER_REVISION_DECISION_OPERATION_KIND, Self::Cancellation => ORDER_CANCELLATION_OPERATION_KIND, - Self::FulfillmentUpdate => ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, - Self::ReceiptRecord => ORDER_RECEIPT_RECORD_OPERATION_KIND, } } @@ -115,8 +99,6 @@ impl OrderWorkflowKind { Self::RevisionProposal => ORDER_REVISION_PROPOSAL_CONTRACT_ID, Self::RevisionDecision => ORDER_REVISION_DECISION_CONTRACT_ID, Self::Cancellation => ORDER_CANCELLATION_CONTRACT_ID, - Self::FulfillmentUpdate => ORDER_FULFILLMENT_UPDATE_CONTRACT_ID, - Self::ReceiptRecord => ORDER_RECEIPT_CONTRACT_ID, } } } @@ -1070,555 +1052,159 @@ pub struct OrderCancellationReceipt { } #[cfg(feature = "runtime")] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] #[non_exhaustive] -pub struct OrderFulfillmentUpdatePrepareRequest { - pub actor: RadrootsActorContext, - pub root_event: RadrootsNostrEventPtr, - pub previous_event: RadrootsNostrEventPtr, - pub fulfillment: RadrootsOrderFulfillmentUpdate, - pub created_at: Option<RadrootsSdkTimestamp>, -} - -#[cfg(feature = "runtime")] -impl serde::Serialize for OrderFulfillmentUpdatePrepareRequest { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("OrderFulfillmentUpdatePrepareRequest", 5)?; - state.serialize_field("actor", &SdkActorContextJson(&self.actor))?; - state.serialize_field("root_event", &self.root_event)?; - state.serialize_field("previous_event", &self.previous_event)?; - state.serialize_field("fulfillment", &self.fulfillment)?; - state.serialize_field("created_at", &self.created_at)?; - state.end() - } +pub struct OrderStatusRequest { + pub order_id: RadrootsOrderId, + pub limit: u32, } #[cfg(feature = "runtime")] -impl OrderFulfillmentUpdatePrepareRequest { - pub fn new( - actor: RadrootsActorContext, - root_event: RadrootsNostrEventPtr, - previous_event: RadrootsNostrEventPtr, - fulfillment: RadrootsOrderFulfillmentUpdate, - ) -> Self { +impl OrderStatusRequest { + pub fn new(order_id: RadrootsOrderId) -> Self { Self { - actor, - root_event, - previous_event, - fulfillment, - created_at: None, + order_id, + limit: ORDER_STATUS_DEFAULT_LIMIT, } } - pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self { - self.created_at = Some(created_at); + pub fn parse(order_id: &str) -> Result<Self, RadrootsSdkError> { + RadrootsOrderId::parse(order_id) + .map(Self::new) + .map_err(|error| RadrootsSdkError::invalid_order_id(order_id, error.to_string())) + } + + pub fn with_limit(mut self, limit: u32) -> Self { + self.limit = limit; self } + + fn validate(&self) -> Result<(), RadrootsSdkError> { + if self.limit == 0 || self.limit > ORDER_STATUS_MAX_LIMIT { + return Err(RadrootsSdkError::order_status_limit_invalid( + self.limit, + 1, + ORDER_STATUS_MAX_LIMIT, + )); + } + Ok(()) + } } #[cfg(feature = "runtime")] -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct OrderFulfillmentUpdateEnqueueRequest { - pub actor: RadrootsActorContext, - pub root_event: RadrootsNostrEventPtr, - pub previous_event: RadrootsNostrEventPtr, - pub fulfillment: RadrootsOrderFulfillmentUpdate, - pub target_relays: SdkRelayTargetPolicy, - pub idempotency_key: Option<SdkIdempotencyKey>, - pub created_at: Option<RadrootsSdkTimestamp>, +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct OrderStatusReceipt { + pub order_id: RadrootsOrderId, + pub source: SdkOrderStatusSource, + pub found: bool, + pub event_count: usize, + pub limit_applied: u32, + pub status: OrderStatusKind, + pub lifecycle_terminal: bool, + pub listing_addr: Option<RadrootsListingAddress>, + pub buyer_pubkey: Option<RadrootsPublicKey>, + pub seller_pubkey: Option<RadrootsPublicKey>, + pub economics: Option<RadrootsOrderEconomics>, + pub evidence: OrderStatusEvidenceSummary, + pub eligibility: OrderStatusEligibility, + pub next_action: OrderStatusNextActionKind, + pub event_ids: Vec<RadrootsEventId>, + pub request_event_id: Option<RadrootsEventId>, + pub decision_event_id: Option<RadrootsEventId>, + pub agreement_event_id: Option<RadrootsEventId>, + pub pending_revision_event_id: Option<RadrootsEventId>, + pub cancellation_event_id: Option<RadrootsEventId>, + pub last_event_id: Option<RadrootsEventId>, + pub issues: Vec<SdkOrderStatusIssue>, } #[cfg(feature = "runtime")] -impl serde::Serialize for OrderFulfillmentUpdateEnqueueRequest { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("OrderFulfillmentUpdateEnqueueRequest", 7)?; - state.serialize_field("actor", &SdkActorContextJson(&self.actor))?; - state.serialize_field("root_event", &self.root_event)?; - state.serialize_field("previous_event", &self.previous_event)?; - state.serialize_field("fulfillment", &self.fulfillment)?; - state.serialize_field("target_relays", &self.target_relays)?; - state.serialize_field("idempotency_key", &self.idempotency_key)?; - state.serialize_field("created_at", &self.created_at)?; - state.end() - } +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct OrderStatusEvidenceSummary { + 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_cancellation: bool, + pub has_issues: bool, } #[cfg(feature = "runtime")] -impl OrderFulfillmentUpdateEnqueueRequest { - pub fn new( - actor: RadrootsActorContext, - root_event: RadrootsNostrEventPtr, - previous_event: RadrootsNostrEventPtr, - fulfillment: RadrootsOrderFulfillmentUpdate, - target_relays: SdkRelayTargetPolicy, - ) -> Self { - Self { - actor, - root_event, - previous_event, - fulfillment, - target_relays, - idempotency_key: None, - created_at: None, - } - } - - pub fn try_with_target_relays<I, S>( - mut self, - target_relays: I, - policy: SdkRelayUrlPolicy, - ) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, policy)?; - Ok(self) - } - - pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct OrderStatusEligibility { + pub can_decide: bool, + pub can_propose_revision: bool, + pub can_decide_revision: bool, + pub can_cancel: bool, +} - pub fn try_with_idempotency_key( - mut self, - idempotency_key: impl AsRef<str>, - ) -> Result<Self, RadrootsSdkError> { - self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?); - Ok(self) - } +#[cfg(feature = "runtime")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum OrderStatusNextActionKind { + NoLocalOrder, + InspectEvidenceIssues, + AwaitSellerDecision, + DecideRevision, + Terminal, +} - pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self { - self.created_at = Some(created_at); - self - } +#[cfg(feature = "runtime")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum SdkOrderStatusSource { + LocalEventStore, } #[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct OrderFulfillmentUpdatePlan { - pub workflow: OrderWorkflowPlan, - pub order_id: RadrootsOrderId, - pub listing_addr: RadrootsListingAddress, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, - pub root_event_id: RadrootsEventId, - pub previous_event_id: RadrootsEventId, - pub expected_event_id: RadrootsEventId, - pub frozen_draft: RadrootsFrozenEventDraft, - pub created_at: RadrootsSdkTimestamp, +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum OrderStatusKind { + Missing, + Requested, + Accepted, + Declined, + Cancelled, + Invalid, } #[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct OrderFulfillmentUpdateReceipt { - pub workflow: OrderWorkflowEnqueueReceipt, - pub order_id: RadrootsOrderId, - pub listing_addr: RadrootsListingAddress, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, - pub root_event_id: RadrootsEventId, - pub previous_event_id: RadrootsEventId, - pub expected_event_id: RadrootsEventId, - pub signed_event_id: RadrootsEventId, - pub local_event_seq: i64, - pub outbox_operation_id: i64, - pub outbox_event_id: i64, - pub state: SdkMutationState, - pub idempotency_digest_prefix: Option<String>, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SdkOrderStatusIssue { + pub kind: SdkOrderStatusIssueKind, + pub event_ids: Vec<RadrootsEventId>, } #[cfg(feature = "runtime")] -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct OrderReceiptRecordPrepareRequest { - pub actor: RadrootsActorContext, - pub root_event: RadrootsNostrEventPtr, - pub previous_event: RadrootsNostrEventPtr, - pub receipt: RadrootsOrderReceipt, - pub created_at: Option<RadrootsSdkTimestamp>, +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]) + } + + pub fn code(&self) -> String { + self.kind.code() + } } #[cfg(feature = "runtime")] -impl serde::Serialize for OrderReceiptRecordPrepareRequest { +impl serde::Serialize for SdkOrderStatusIssue { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { - let mut state = serializer.serialize_struct("OrderReceiptRecordPrepareRequest", 5)?; - state.serialize_field("actor", &SdkActorContextJson(&self.actor))?; - state.serialize_field("root_event", &self.root_event)?; - state.serialize_field("previous_event", &self.previous_event)?; - state.serialize_field("receipt", &self.receipt)?; - state.serialize_field("created_at", &self.created_at)?; - state.end() - } -} - -#[cfg(feature = "runtime")] -impl OrderReceiptRecordPrepareRequest { - pub fn new( - actor: RadrootsActorContext, - root_event: RadrootsNostrEventPtr, - previous_event: RadrootsNostrEventPtr, - receipt: RadrootsOrderReceipt, - ) -> Self { - Self { - actor, - root_event, - previous_event, - receipt, - created_at: None, - } - } - - pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self { - self.created_at = Some(created_at); - self - } -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct OrderReceiptRecordEnqueueRequest { - pub actor: RadrootsActorContext, - pub root_event: RadrootsNostrEventPtr, - pub previous_event: RadrootsNostrEventPtr, - pub receipt: RadrootsOrderReceipt, - pub target_relays: SdkRelayTargetPolicy, - pub idempotency_key: Option<SdkIdempotencyKey>, - pub created_at: Option<RadrootsSdkTimestamp>, -} - -#[cfg(feature = "runtime")] -impl serde::Serialize for OrderReceiptRecordEnqueueRequest { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("OrderReceiptRecordEnqueueRequest", 7)?; - state.serialize_field("actor", &SdkActorContextJson(&self.actor))?; - state.serialize_field("root_event", &self.root_event)?; - state.serialize_field("previous_event", &self.previous_event)?; - state.serialize_field("receipt", &self.receipt)?; - state.serialize_field("target_relays", &self.target_relays)?; - state.serialize_field("idempotency_key", &self.idempotency_key)?; - state.serialize_field("created_at", &self.created_at)?; - state.end() - } -} - -#[cfg(feature = "runtime")] -impl OrderReceiptRecordEnqueueRequest { - pub fn new( - actor: RadrootsActorContext, - root_event: RadrootsNostrEventPtr, - previous_event: RadrootsNostrEventPtr, - receipt: RadrootsOrderReceipt, - target_relays: SdkRelayTargetPolicy, - ) -> Self { - Self { - actor, - root_event, - previous_event, - receipt, - target_relays, - idempotency_key: None, - created_at: None, - } - } - - pub fn try_with_target_relays<I, S>( - mut self, - target_relays: I, - policy: SdkRelayUrlPolicy, - ) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, policy)?; - Ok(self) - } - - pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn try_with_idempotency_key( - mut self, - idempotency_key: impl AsRef<str>, - ) -> Result<Self, RadrootsSdkError> { - self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?); - Ok(self) - } - - pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self { - self.created_at = Some(created_at); - self - } -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct OrderReceiptRecordPlan { - pub workflow: OrderWorkflowPlan, - pub order_id: RadrootsOrderId, - pub listing_addr: RadrootsListingAddress, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, - pub root_event_id: RadrootsEventId, - pub previous_event_id: RadrootsEventId, - pub expected_event_id: RadrootsEventId, - pub frozen_draft: RadrootsFrozenEventDraft, - pub created_at: RadrootsSdkTimestamp, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct OrderReceiptRecordReceipt { - pub workflow: OrderWorkflowEnqueueReceipt, - pub order_id: RadrootsOrderId, - pub listing_addr: RadrootsListingAddress, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, - pub root_event_id: RadrootsEventId, - pub previous_event_id: RadrootsEventId, - pub expected_event_id: RadrootsEventId, - pub signed_event_id: RadrootsEventId, - pub local_event_seq: i64, - pub outbox_operation_id: i64, - pub outbox_event_id: i64, - pub state: SdkMutationState, - pub idempotency_digest_prefix: Option<String>, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -#[non_exhaustive] -pub struct OrderStatusRequest { - pub order_id: RadrootsOrderId, - pub limit: u32, -} - -#[cfg(feature = "runtime")] -impl OrderStatusRequest { - pub fn new(order_id: RadrootsOrderId) -> Self { - Self { - 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::invalid_order_id(order_id, error.to_string())) - } - - pub fn with_limit(mut self, limit: u32) -> Self { - self.limit = limit; - self - } - - fn validate(&self) -> Result<(), RadrootsSdkError> { - if self.limit == 0 || self.limit > ORDER_STATUS_MAX_LIMIT { - return Err(RadrootsSdkError::order_status_limit_invalid( - self.limit, - 1, - ORDER_STATUS_MAX_LIMIT, - )); - } - Ok(()) - } -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct OrderStatusReceipt { - 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 evidence: OrderStatusEvidenceSummary, - pub eligibility: OrderStatusEligibility, - pub payment_handoff: OrderPaymentHandoffKind, - pub next_action: OrderStatusNextActionKind, - pub event_ids: Vec<RadrootsEventId>, - pub request_event_id: Option<RadrootsEventId>, - pub decision_event_id: Option<RadrootsEventId>, - pub agreement_event_id: Option<RadrootsEventId>, - pub pending_revision_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, Debug, PartialEq, Eq, serde::Serialize)] -pub struct OrderStatusEvidenceSummary { - 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, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct OrderStatusEligibility { - 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, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum OrderPaymentHandoffKind { - NotReady, - NotRequired, - InPersonOrOffPlatformPending, - InPersonOrOffPlatformRecorded, - InPersonOrOffPlatformSettled, - Rejected, - Invalid, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum OrderStatusNextActionKind { - NoLocalOrder, - InspectEvidenceIssues, - AwaitSellerDecision, - ArrangeInPersonOrOffPlatformPayment, - DecideRevision, - FulfillOrder, - RecordReceipt, - Terminal, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum SdkOrderStatusSource { - LocalEventStore, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum OrderStatusKind { - Missing, - Requested, - Accepted, - Declined, - Cancelled, - Completed, - Disputed, - Invalid, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum OrderFulfillmentStatusKind { - AcceptedNotFulfilled, - Preparing, - ReadyForPickup, - OutForDelivery, - Delivered, - SellerCancelled, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum OrderPaymentStateKind { - NotRecorded, - Recorded, - Settled, - Rejected, - Invalid, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum OrderSettlementStateKind { - NotRequired, - Pending, - Accepted, - Rejected, - Invalid, -} - -#[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]) - } - - pub fn code(&self) -> String { - self.kind.code() - } -} - -#[cfg(feature = "runtime")] -impl serde::Serialize for SdkOrderStatusIssue { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("SdkOrderStatusIssue", 3)?; - state.serialize_field("code", &self.code())?; - state.serialize_field("kind", &self.kind)?; - state.serialize_field("event_ids", &self.event_ids)?; + let mut state = serializer.serialize_struct("SdkOrderStatusIssue", 3)?; + state.serialize_field("code", &self.code())?; + state.serialize_field("kind", &self.kind)?; + state.serialize_field("event_ids", &self.event_ids)?; state.end() } } @@ -1649,7 +1235,6 @@ pub enum SdkOrderStatusIssueKind { DecisionInventoryCommitmentMismatch, DecisionMissingReason, ConflictingDecisions, - RevisionProposalWithoutAcceptedDecision, RevisionProposalPayloadInvalid, RevisionProposalOrderIdMismatch, RevisionProposalAuthorMismatch, @@ -1672,20 +1257,6 @@ pub enum SdkOrderStatusIssueKind { RevisionDecisionRootMismatch, RevisionDecisionPreviousMismatch, RevisionDecisionRevisionIdMismatch, - FulfillmentWithoutAcceptedDecision, - FulfillmentPayloadInvalid, - FulfillmentOrderIdMismatch, - FulfillmentAuthorMismatch, - FulfillmentCounterpartyMismatch, - FulfillmentBuyerMismatch, - FulfillmentSellerMismatch, - FulfillmentListingAddressInvalid, - FulfillmentListingMismatch, - FulfillmentRootMismatch, - FulfillmentPreviousMismatch, - FulfillmentStatusNotPublishable, - FulfillmentUnsupportedTransition, - ForkedFulfillments, CancellationWithoutCancellableOrder, CancellationPayloadInvalid, CancellationOrderIdMismatch, @@ -1697,57 +1268,6 @@ pub enum SdkOrderStatusIssueKind { 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, } @@ -1953,206 +1473,7 @@ impl<'sdk> OrdersClient<'sdk> { let enqueue = enqueue_signed_workflow( self.sdk, SdkWorkflowEnqueueRequest { - operation_kind: OrderWorkflowKind::Decision.operation_kind(), - actor, - frozen_draft: &plan.frozen_draft, - target_relays, - idempotency_key, - }, - signer, - ) - .await?; - Ok(OrderDecisionReceipt { - workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::Decision, - plan.expected_event_id.clone(), - &enqueue, - ), - order_id: plan.order_id, - listing_addr: plan.listing_addr, - buyer_pubkey: plan.buyer_pubkey, - seller_pubkey: plan.seller_pubkey, - request_event_id: plan.request_event_id, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) - } - - pub fn prepare_revision_proposal( - &self, - request: OrderRevisionProposalPrepareRequest, - ) -> Result<OrderRevisionProposalPlan, RadrootsSdkError> { - let created_at = self.resolved_created_at(request.created_at)?; - order_revision_proposal_plan( - &request.actor, - request.root_event, - request.previous_event, - request.proposal, - created_at, - ) - } - - pub async fn enqueue_revision_proposal<S>( - &self, - request: OrderRevisionProposalEnqueueRequest, - signer: &S, - ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> - where - S: RadrootsEventSigner + ?Sized, - { - let OrderRevisionProposalEnqueueRequest { - actor, - root_event, - previous_event, - proposal, - target_relays, - idempotency_key, - created_at, - } = request; - let prepare_request = OrderRevisionProposalPrepareRequest { - actor: actor.clone(), - root_event, - previous_event, - proposal, - created_at, - }; - let plan = self.prepare_revision_proposal(prepare_request)?; - self.enqueue_prepared_revision_proposal( - &actor, - plan, - target_relays, - idempotency_key, - signer, - ) - .await - } - - pub async fn enqueue_prepared_revision_proposal<S>( - &self, - actor: &RadrootsActorContext, - plan: OrderRevisionProposalPlan, - target_relays: SdkRelayTargetPolicy, - idempotency_key: Option<SdkIdempotencyKey>, - signer: &S, - ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> - where - S: RadrootsEventSigner + ?Sized, - { - if !self - .prepared_order_event_exists(&plan.expected_event_id) - .await? - { - self.require_revision_proposal_preflight(&plan).await?; - } - let enqueue = enqueue_signed_workflow( - self.sdk, - SdkWorkflowEnqueueRequest { - operation_kind: OrderWorkflowKind::RevisionProposal.operation_kind(), - actor, - frozen_draft: &plan.frozen_draft, - target_relays, - idempotency_key, - }, - signer, - ) - .await?; - Ok(OrderRevisionProposalReceipt { - workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::RevisionProposal, - plan.expected_event_id.clone(), - &enqueue, - ), - order_id: plan.order_id, - listing_addr: plan.listing_addr, - buyer_pubkey: plan.buyer_pubkey, - seller_pubkey: plan.seller_pubkey, - root_event_id: plan.root_event_id, - previous_event_id: plan.previous_event_id, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) - } - - pub fn prepare_revision_decision( - &self, - request: OrderRevisionDecisionPrepareRequest, - ) -> Result<OrderRevisionDecisionPlan, RadrootsSdkError> { - let created_at = self.resolved_created_at(request.created_at)?; - order_revision_decision_plan( - &request.actor, - request.root_event, - request.previous_event, - request.decision, - created_at, - ) - } - - pub async fn enqueue_revision_decision<S>( - &self, - request: OrderRevisionDecisionEnqueueRequest, - signer: &S, - ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> - where - S: RadrootsEventSigner + ?Sized, - { - let OrderRevisionDecisionEnqueueRequest { - actor, - root_event, - previous_event, - decision, - target_relays, - idempotency_key, - created_at, - } = request; - let prepare_request = OrderRevisionDecisionPrepareRequest { - actor: actor.clone(), - root_event, - previous_event, - decision, - created_at, - }; - let plan = self.prepare_revision_decision(prepare_request)?; - self.enqueue_prepared_revision_decision( - &actor, - plan, - target_relays, - idempotency_key, - signer, - ) - .await - } - - pub async fn enqueue_prepared_revision_decision<S>( - &self, - actor: &RadrootsActorContext, - plan: OrderRevisionDecisionPlan, - target_relays: SdkRelayTargetPolicy, - idempotency_key: Option<SdkIdempotencyKey>, - signer: &S, - ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> - where - S: RadrootsEventSigner + ?Sized, - { - if !self - .prepared_order_event_exists(&plan.expected_event_id) - .await? - { - self.require_revision_decision_preflight(&plan).await?; - } - let enqueue = enqueue_signed_workflow( - self.sdk, - SdkWorkflowEnqueueRequest { - operation_kind: OrderWorkflowKind::RevisionDecision.operation_kind(), + operation_kind: OrderWorkflowKind::Decision.operation_kind(), actor, frozen_draft: &plan.frozen_draft, target_relays, @@ -2161,9 +1482,9 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderRevisionDecisionReceipt { + Ok(OrderDecisionReceipt { workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::RevisionDecision, + OrderWorkflowKind::Decision, plan.expected_event_id.clone(), &enqueue, ), @@ -2171,8 +1492,7 @@ impl<'sdk> OrdersClient<'sdk> { listing_addr: plan.listing_addr, buyer_pubkey: plan.buyer_pubkey, seller_pubkey: plan.seller_pubkey, - root_event_id: plan.root_event_id, - previous_event_id: plan.previous_event_id, + request_event_id: plan.request_event_id, expected_event_id: plan.expected_event_id, signed_event_id: enqueue.signed_event_id, local_event_seq: enqueue.local_event_seq, @@ -2183,57 +1503,63 @@ impl<'sdk> OrdersClient<'sdk> { }) } - pub fn prepare_cancellation( + pub fn prepare_revision_proposal( &self, - request: OrderCancellationPrepareRequest, - ) -> Result<OrderCancellationPlan, RadrootsSdkError> { + request: OrderRevisionProposalPrepareRequest, + ) -> Result<OrderRevisionProposalPlan, RadrootsSdkError> { let created_at = self.resolved_created_at(request.created_at)?; - order_cancellation_plan( + order_revision_proposal_plan( &request.actor, request.root_event, request.previous_event, - request.cancellation, + request.proposal, created_at, ) } - pub async fn enqueue_cancellation<S>( + pub async fn enqueue_revision_proposal<S>( &self, - request: OrderCancellationEnqueueRequest, + request: OrderRevisionProposalEnqueueRequest, signer: &S, - ) -> Result<OrderCancellationReceipt, RadrootsSdkError> + ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> where S: RadrootsEventSigner + ?Sized, { - let OrderCancellationEnqueueRequest { + let OrderRevisionProposalEnqueueRequest { actor, root_event, previous_event, - cancellation, + proposal, target_relays, idempotency_key, created_at, } = request; - let prepare_request = OrderCancellationPrepareRequest { + let prepare_request = OrderRevisionProposalPrepareRequest { actor: actor.clone(), root_event, previous_event, - cancellation, + proposal, created_at, }; - let plan = self.prepare_cancellation(prepare_request)?; - self.enqueue_prepared_cancellation(&actor, plan, target_relays, idempotency_key, signer) - .await + let plan = self.prepare_revision_proposal(prepare_request)?; + self.enqueue_prepared_revision_proposal( + &actor, + plan, + target_relays, + idempotency_key, + signer, + ) + .await } - pub async fn enqueue_prepared_cancellation<S>( + pub async fn enqueue_prepared_revision_proposal<S>( &self, actor: &RadrootsActorContext, - plan: OrderCancellationPlan, + plan: OrderRevisionProposalPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &S, - ) -> Result<OrderCancellationReceipt, RadrootsSdkError> + ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> where S: RadrootsEventSigner + ?Sized, { @@ -2241,12 +1567,12 @@ impl<'sdk> OrdersClient<'sdk> { .prepared_order_event_exists(&plan.expected_event_id) .await? { - self.require_cancellation_preflight(&plan).await?; + self.require_revision_proposal_preflight(&plan).await?; } let enqueue = enqueue_signed_workflow( self.sdk, SdkWorkflowEnqueueRequest { - operation_kind: OrderWorkflowKind::Cancellation.operation_kind(), + operation_kind: OrderWorkflowKind::RevisionProposal.operation_kind(), actor, frozen_draft: &plan.frozen_draft, target_relays, @@ -2255,9 +1581,9 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderCancellationReceipt { + Ok(OrderRevisionProposalReceipt { workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::Cancellation, + OrderWorkflowKind::RevisionProposal, plan.expected_event_id.clone(), &enqueue, ), @@ -2277,46 +1603,46 @@ impl<'sdk> OrdersClient<'sdk> { }) } - pub fn prepare_fulfillment_update( + pub fn prepare_revision_decision( &self, - request: OrderFulfillmentUpdatePrepareRequest, - ) -> Result<OrderFulfillmentUpdatePlan, RadrootsSdkError> { + request: OrderRevisionDecisionPrepareRequest, + ) -> Result<OrderRevisionDecisionPlan, RadrootsSdkError> { let created_at = self.resolved_created_at(request.created_at)?; - order_fulfillment_update_plan( + order_revision_decision_plan( &request.actor, request.root_event, request.previous_event, - request.fulfillment, + request.decision, created_at, ) } - pub async fn enqueue_fulfillment_update<S>( + pub async fn enqueue_revision_decision<S>( &self, - request: OrderFulfillmentUpdateEnqueueRequest, + request: OrderRevisionDecisionEnqueueRequest, signer: &S, - ) -> Result<OrderFulfillmentUpdateReceipt, RadrootsSdkError> + ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> where S: RadrootsEventSigner + ?Sized, { - let OrderFulfillmentUpdateEnqueueRequest { + let OrderRevisionDecisionEnqueueRequest { actor, root_event, previous_event, - fulfillment, + decision, target_relays, idempotency_key, created_at, } = request; - let prepare_request = OrderFulfillmentUpdatePrepareRequest { + let prepare_request = OrderRevisionDecisionPrepareRequest { actor: actor.clone(), root_event, previous_event, - fulfillment, + decision, created_at, }; - let plan = self.prepare_fulfillment_update(prepare_request)?; - self.enqueue_prepared_fulfillment_update( + let plan = self.prepare_revision_decision(prepare_request)?; + self.enqueue_prepared_revision_decision( &actor, plan, target_relays, @@ -2326,14 +1652,14 @@ impl<'sdk> OrdersClient<'sdk> { .await } - pub async fn enqueue_prepared_fulfillment_update<S>( + pub async fn enqueue_prepared_revision_decision<S>( &self, actor: &RadrootsActorContext, - plan: OrderFulfillmentUpdatePlan, + plan: OrderRevisionDecisionPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &S, - ) -> Result<OrderFulfillmentUpdateReceipt, RadrootsSdkError> + ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> where S: RadrootsEventSigner + ?Sized, { @@ -2341,12 +1667,12 @@ impl<'sdk> OrdersClient<'sdk> { .prepared_order_event_exists(&plan.expected_event_id) .await? { - self.require_fulfillment_update_preflight(&plan).await?; + self.require_revision_decision_preflight(&plan).await?; } let enqueue = enqueue_signed_workflow( self.sdk, SdkWorkflowEnqueueRequest { - operation_kind: OrderWorkflowKind::FulfillmentUpdate.operation_kind(), + operation_kind: OrderWorkflowKind::RevisionDecision.operation_kind(), actor, frozen_draft: &plan.frozen_draft, target_relays, @@ -2355,9 +1681,9 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderFulfillmentUpdateReceipt { + Ok(OrderRevisionDecisionReceipt { workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::FulfillmentUpdate, + OrderWorkflowKind::RevisionDecision, plan.expected_event_id.clone(), &enqueue, ), @@ -2377,57 +1703,57 @@ impl<'sdk> OrdersClient<'sdk> { }) } - pub fn prepare_receipt_record( + pub fn prepare_cancellation( &self, - request: OrderReceiptRecordPrepareRequest, - ) -> Result<OrderReceiptRecordPlan, RadrootsSdkError> { + request: OrderCancellationPrepareRequest, + ) -> Result<OrderCancellationPlan, RadrootsSdkError> { let created_at = self.resolved_created_at(request.created_at)?; - order_receipt_record_plan( + order_cancellation_plan( &request.actor, request.root_event, request.previous_event, - request.receipt, + request.cancellation, created_at, ) } - pub async fn enqueue_receipt_record<S>( + pub async fn enqueue_cancellation<S>( &self, - request: OrderReceiptRecordEnqueueRequest, + request: OrderCancellationEnqueueRequest, signer: &S, - ) -> Result<OrderReceiptRecordReceipt, RadrootsSdkError> + ) -> Result<OrderCancellationReceipt, RadrootsSdkError> where S: RadrootsEventSigner + ?Sized, { - let OrderReceiptRecordEnqueueRequest { + let OrderCancellationEnqueueRequest { actor, root_event, previous_event, - receipt, + cancellation, target_relays, idempotency_key, created_at, } = request; - let prepare_request = OrderReceiptRecordPrepareRequest { + let prepare_request = OrderCancellationPrepareRequest { actor: actor.clone(), root_event, previous_event, - receipt, + cancellation, created_at, }; - let plan = self.prepare_receipt_record(prepare_request)?; - self.enqueue_prepared_receipt_record(&actor, plan, target_relays, idempotency_key, signer) + let plan = self.prepare_cancellation(prepare_request)?; + self.enqueue_prepared_cancellation(&actor, plan, target_relays, idempotency_key, signer) .await } - pub async fn enqueue_prepared_receipt_record<S>( + pub async fn enqueue_prepared_cancellation<S>( &self, actor: &RadrootsActorContext, - plan: OrderReceiptRecordPlan, + plan: OrderCancellationPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &S, - ) -> Result<OrderReceiptRecordReceipt, RadrootsSdkError> + ) -> Result<OrderCancellationReceipt, RadrootsSdkError> where S: RadrootsEventSigner + ?Sized, { @@ -2435,12 +1761,12 @@ impl<'sdk> OrdersClient<'sdk> { .prepared_order_event_exists(&plan.expected_event_id) .await? { - self.require_receipt_record_preflight(&plan).await?; + self.require_cancellation_preflight(&plan).await?; } let enqueue = enqueue_signed_workflow( self.sdk, SdkWorkflowEnqueueRequest { - operation_kind: OrderWorkflowKind::ReceiptRecord.operation_kind(), + operation_kind: OrderWorkflowKind::Cancellation.operation_kind(), actor, frozen_draft: &plan.frozen_draft, target_relays, @@ -2449,9 +1775,9 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderReceiptRecordReceipt { + Ok(OrderCancellationReceipt { workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::ReceiptRecord, + OrderWorkflowKind::Cancellation, plan.expected_event_id.clone(), &enqueue, ), @@ -2528,22 +1854,6 @@ impl<'sdk> OrdersClient<'sdk> { require_cancellation_state(plan, &query_result.projection) } - async fn require_fulfillment_update_preflight( - &self, - plan: &OrderFulfillmentUpdatePlan, - ) -> Result<(), RadrootsSdkError> { - let query_result = self.query_order_projection(&plan.order_id).await?; - require_fulfillment_update_state(plan, &query_result.projection) - } - - async fn require_receipt_record_preflight( - &self, - plan: &OrderReceiptRecordPlan, - ) -> Result<(), RadrootsSdkError> { - let query_result = self.query_order_projection(&plan.order_id).await?; - require_receipt_record_state(plan, &query_result.projection) - } - async fn query_order_projection( &self, order_id: &RadrootsOrderId, @@ -2620,30 +1930,6 @@ fn parse_order_evidence( .payload; (payload.order_id, payload.listing_addr) } - KIND_ORDER_FULFILLMENT_UPDATE => { - let payload = order_fulfillment_update_from_event(event) - .map_err(order_evidence_parse_error)? - .payload; - (payload.order_id, payload.listing_addr) - } - KIND_ORDER_RECEIPT => { - let payload = order_receipt_from_event(event) - .map_err(order_evidence_parse_error)? - .payload; - (payload.order_id, payload.listing_addr) - } - KIND_ORDER_PAYMENT_RECORD => { - let payload = order_payment_record_from_event(event) - .map_err(order_evidence_parse_error)? - .payload; - (payload.order_id, payload.listing_addr) - } - KIND_ORDER_SETTLEMENT_DECISION => { - let payload = order_settlement_decision_from_event(event) - .map_err(order_evidence_parse_error)? - .payload; - (payload.order_id, payload.listing_addr) - } other => { return Err(RadrootsSdkError::InvalidRequest { message: format!("order evidence event kind {other} is not supported"), @@ -2679,9 +1965,7 @@ impl OrderStatusReceipt { query_result.limit_applied, ); let eligibility = OrderStatusEligibility::from_projection(&projection); - let payment_handoff = OrderPaymentHandoffKind::from_projection(&projection); - let next_action = - OrderStatusNextActionKind::from_projection(&projection, &eligibility, payment_handoff); + let next_action = OrderStatusNextActionKind::from_projection(&projection, &eligibility); Self { order_id: projection.order_id, source: SdkOrderStatusSource::LocalEventStore, @@ -2689,22 +1973,20 @@ impl OrderStatusReceipt { 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, + listing_addr: projection.listing_addr, + buyer_pubkey: projection.buyer_pubkey, + seller_pubkey: projection.seller_pubkey, + economics: projection.economics, evidence, eligibility, - payment_handoff, next_action, event_ids: query_result.event_ids, request_event_id: projection.request_event_id, decision_event_id: projection.decision_event_id, agreement_event_id: projection.agreement_event_id, pending_revision_event_id: projection.pending_revision_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(), } @@ -2725,9 +2007,7 @@ impl OrderStatusEvidenceSummary { has_decision: projection.decision_event_id.is_some(), has_agreement: projection.agreement_event_id.is_some(), has_pending_revision: projection.pending_revision_event_id.is_some(), - has_fulfillment: projection.fulfillment_event_id.is_some(), has_cancellation: projection.cancellation_event_id.is_some(), - has_receipt: projection.receipt_event_id.is_some(), has_issues: !projection.issues.is_empty(), } } @@ -2739,72 +2019,16 @@ impl OrderStatusEligibility { let clean = projection.issues.is_empty(); let open = clean && !projection.lifecycle_terminal; let requested = projection.status == RadrootsOrderStatus::Requested; - let accepted = projection.status == RadrootsOrderStatus::Accepted; let has_pending_revision = projection.pending_revision_event_id.is_some(); - let has_fulfillment = projection.fulfillment_event_id.is_some(); - let fulfillment_terminal = matches!( - projection.fulfillment_status, - Some( - RadrootsOrderFulfillmentState::Delivered - | RadrootsOrderFulfillmentState::SellerCancelled - ) - ); - let receipt_ready = matches!( - projection.fulfillment_status, - Some( - RadrootsOrderFulfillmentState::ReadyForPickup - | RadrootsOrderFulfillmentState::Delivered - ) - ); - let revision_payment_open = - projection.payment.state == RadrootsOrderPaymentState::NotRecorded; Self { - can_decide: open && requested && projection.decision_event_id.is_none(), - can_propose_revision: open - && accepted - && !has_pending_revision - && !has_fulfillment - && revision_payment_open, - can_decide_revision: open && accepted && has_pending_revision, - can_cancel: open - && matches!( - projection.status, - RadrootsOrderStatus::Requested | RadrootsOrderStatus::Accepted - ) - && !has_pending_revision - && !has_fulfillment, - can_update_fulfillment: open - && accepted - && !has_pending_revision - && !fulfillment_terminal, - can_record_receipt: open - && accepted - && receipt_ready - && projection.receipt_event_id.is_none(), - } - } -} - -#[cfg(feature = "runtime")] -impl OrderPaymentHandoffKind { - fn from_projection(projection: &RadrootsOrderProjection) -> Self { - if !projection.issues.is_empty() || projection.status == RadrootsOrderStatus::Invalid { - return Self::Invalid; - } - match projection.status { - RadrootsOrderStatus::Missing | RadrootsOrderStatus::Requested => Self::NotReady, - RadrootsOrderStatus::Declined | RadrootsOrderStatus::Cancelled => Self::NotRequired, - RadrootsOrderStatus::Accepted - | RadrootsOrderStatus::Completed - | RadrootsOrderStatus::Disputed => match projection.payment.state { - RadrootsOrderPaymentState::NotRecorded => Self::InPersonOrOffPlatformPending, - RadrootsOrderPaymentState::Recorded => Self::InPersonOrOffPlatformRecorded, - RadrootsOrderPaymentState::Settled => Self::InPersonOrOffPlatformSettled, - RadrootsOrderPaymentState::Rejected => Self::Rejected, - RadrootsOrderPaymentState::Invalid => Self::Invalid, - }, - RadrootsOrderStatus::Invalid => Self::Invalid, + can_decide: open + && requested + && projection.decision_event_id.is_none() + && !has_pending_revision, + can_propose_revision: open && requested && !has_pending_revision, + can_decide_revision: open && requested && has_pending_revision, + can_cancel: open && requested && !has_pending_revision, } } } @@ -2814,7 +2038,6 @@ impl OrderStatusNextActionKind { fn from_projection( projection: &RadrootsOrderProjection, eligibility: &OrderStatusEligibility, - payment_handoff: OrderPaymentHandoffKind, ) -> Self { if projection.status == RadrootsOrderStatus::Missing { return Self::NoLocalOrder; @@ -2831,18 +2054,6 @@ impl OrderStatusNextActionKind { if eligibility.can_decide_revision { return Self::DecideRevision; } - if eligibility.can_record_receipt { - return Self::RecordReceipt; - } - if matches!( - payment_handoff, - OrderPaymentHandoffKind::InPersonOrOffPlatformPending - ) { - return Self::ArrangeInPersonOrOffPlatformPayment; - } - if eligibility.can_update_fulfillment { - return Self::FulfillOrder; - } Self::Terminal } } @@ -2978,135 +2189,24 @@ fn order_revision_proposal_plan( )?; let created_at_nostr = created_at.try_into_nostr_created_at()?; let order_id = proposal.order_id.clone(); - let listing_addr = proposal.listing_addr.clone(); - let buyer_pubkey = proposal.buyer_pubkey.clone(); - let seller_pubkey = proposal.seller_pubkey.clone(); - let draft = - order::build_order_revision_proposal_draft(&root_event_id, &previous_event_id, &proposal) - .map_err(|error| RadrootsSdkError::InvalidRequest { - message: format!("order revision proposal draft encode failed: {error}"), - })?; - let (frozen_draft, expected_event_id) = freeze_order_workflow_draft( - draft.into_wire_parts(), - ORDER_REVISION_PROPOSAL_CONTRACT_ID, - seller_pubkey.as_str(), - created_at_nostr, - "order revision proposal", - )?; - Ok(OrderRevisionProposalPlan { - workflow: order_workflow_plan( - OrderWorkflowKind::RevisionProposal, - expected_event_id.clone(), - created_at, - ), - order_id, - listing_addr, - buyer_pubkey, - seller_pubkey, - root_event_id, - previous_event_id, - expected_event_id, - frozen_draft, - created_at, - }) -} - -#[cfg(feature = "runtime")] -fn order_revision_decision_plan( - actor: &RadrootsActorContext, - root_event: RadrootsNostrEventPtr, - previous_event: RadrootsNostrEventPtr, - decision: RadrootsOrderRevisionDecision, - created_at: RadrootsSdkTimestamp, -) -> Result<OrderRevisionDecisionPlan, RadrootsSdkError> { - require_buyer_actor(actor, "order.prepare_revision_decision")?; - let root_event_id = order_reference_event_id(&root_event, "root")?; - let previous_event_id = order_reference_event_id(&previous_event, "previous")?; - if decision.buyer_pubkey.as_str() != actor.pubkey().as_str() { - return Err(RadrootsSdkError::UnauthorizedActor { - operation: "order.prepare_revision_decision".to_owned(), - reason: "actor pubkey must match order buyer_pubkey".to_owned(), - }); - } - require_payload_event_refs( - "order revision decision", - &decision.root_event_id, - &decision.prev_event_id, - &root_event_id, - &previous_event_id, - )?; - let created_at_nostr = created_at.try_into_nostr_created_at()?; - let order_id = decision.order_id.clone(); - let listing_addr = decision.listing_addr.clone(); - let buyer_pubkey = decision.buyer_pubkey.clone(); - let seller_pubkey = decision.seller_pubkey.clone(); - let draft = - order::build_order_revision_decision_draft(&root_event_id, &previous_event_id, &decision) - .map_err(|error| RadrootsSdkError::InvalidRequest { - message: format!("order revision decision draft encode failed: {error}"), - })?; - let (frozen_draft, expected_event_id) = freeze_order_workflow_draft( - draft.into_wire_parts(), - ORDER_REVISION_DECISION_CONTRACT_ID, - buyer_pubkey.as_str(), - created_at_nostr, - "order revision decision", - )?; - Ok(OrderRevisionDecisionPlan { - workflow: order_workflow_plan( - OrderWorkflowKind::RevisionDecision, - expected_event_id.clone(), - created_at, - ), - order_id, - listing_addr, - buyer_pubkey, - seller_pubkey, - root_event_id, - previous_event_id, - expected_event_id, - frozen_draft, - created_at, - }) -} - -#[cfg(feature = "runtime")] -fn order_cancellation_plan( - actor: &RadrootsActorContext, - root_event: RadrootsNostrEventPtr, - previous_event: RadrootsNostrEventPtr, - cancellation: RadrootsOrderCancellation, - created_at: RadrootsSdkTimestamp, -) -> Result<OrderCancellationPlan, RadrootsSdkError> { - require_buyer_actor(actor, "order.prepare_cancellation")?; - let root_event_id = order_reference_event_id(&root_event, "root")?; - let previous_event_id = order_reference_event_id(&previous_event, "previous")?; - if cancellation.buyer_pubkey.as_str() != actor.pubkey().as_str() { - return Err(RadrootsSdkError::UnauthorizedActor { - operation: "order.prepare_cancellation".to_owned(), - reason: "actor pubkey must match order buyer_pubkey".to_owned(), - }); - } - let created_at_nostr = created_at.try_into_nostr_created_at()?; - let order_id = cancellation.order_id.clone(); - let listing_addr = cancellation.listing_addr.clone(); - let buyer_pubkey = cancellation.buyer_pubkey.clone(); - let seller_pubkey = cancellation.seller_pubkey.clone(); + let listing_addr = proposal.listing_addr.clone(); + let buyer_pubkey = proposal.buyer_pubkey.clone(); + let seller_pubkey = proposal.seller_pubkey.clone(); let draft = - order::build_order_cancellation_draft(&root_event_id, &previous_event_id, &cancellation) + order::build_order_revision_proposal_draft(&root_event_id, &previous_event_id, &proposal) .map_err(|error| RadrootsSdkError::InvalidRequest { - message: format!("order cancellation draft encode failed: {error}"), - })?; + message: format!("order revision proposal draft encode failed: {error}"), + })?; let (frozen_draft, expected_event_id) = freeze_order_workflow_draft( draft.into_wire_parts(), - ORDER_CANCELLATION_CONTRACT_ID, - buyer_pubkey.as_str(), + ORDER_REVISION_PROPOSAL_CONTRACT_ID, + seller_pubkey.as_str(), created_at_nostr, - "order cancellation", + "order revision proposal", )?; - Ok(OrderCancellationPlan { + Ok(OrderRevisionProposalPlan { workflow: order_workflow_plan( - OrderWorkflowKind::Cancellation, + OrderWorkflowKind::RevisionProposal, expected_event_id.clone(), created_at, ), @@ -3123,42 +2223,49 @@ fn order_cancellation_plan( } #[cfg(feature = "runtime")] -fn order_fulfillment_update_plan( +fn order_revision_decision_plan( actor: &RadrootsActorContext, root_event: RadrootsNostrEventPtr, previous_event: RadrootsNostrEventPtr, - fulfillment: RadrootsOrderFulfillmentUpdate, + decision: RadrootsOrderRevisionDecision, created_at: RadrootsSdkTimestamp, -) -> Result<OrderFulfillmentUpdatePlan, RadrootsSdkError> { - require_seller_actor(actor, "order.prepare_fulfillment_update")?; +) -> Result<OrderRevisionDecisionPlan, RadrootsSdkError> { + require_buyer_actor(actor, "order.prepare_revision_decision")?; let root_event_id = order_reference_event_id(&root_event, "root")?; let previous_event_id = order_reference_event_id(&previous_event, "previous")?; - if fulfillment.seller_pubkey.as_str() != actor.pubkey().as_str() { + if decision.buyer_pubkey.as_str() != actor.pubkey().as_str() { return Err(RadrootsSdkError::UnauthorizedActor { - operation: "order.prepare_fulfillment_update".to_owned(), - reason: "actor pubkey must match order seller_pubkey".to_owned(), + operation: "order.prepare_revision_decision".to_owned(), + reason: "actor pubkey must match order buyer_pubkey".to_owned(), }); } + require_payload_event_refs( + "order revision decision", + &decision.root_event_id, + &decision.prev_event_id, + &root_event_id, + &previous_event_id, + )?; let created_at_nostr = created_at.try_into_nostr_created_at()?; - let order_id = fulfillment.order_id.clone(); - let listing_addr = fulfillment.listing_addr.clone(); - let buyer_pubkey = fulfillment.buyer_pubkey.clone(); - let seller_pubkey = fulfillment.seller_pubkey.clone(); + let order_id = decision.order_id.clone(); + let listing_addr = decision.listing_addr.clone(); + let buyer_pubkey = decision.buyer_pubkey.clone(); + let seller_pubkey = decision.seller_pubkey.clone(); let draft = - order::build_fulfillment_update_draft(&root_event_id, &previous_event_id, &fulfillment) + order::build_order_revision_decision_draft(&root_event_id, &previous_event_id, &decision) .map_err(|error| RadrootsSdkError::InvalidRequest { - message: format!("order fulfillment update draft encode failed: {error}"), - })?; + message: format!("order revision decision draft encode failed: {error}"), + })?; let (frozen_draft, expected_event_id) = freeze_order_workflow_draft( draft.into_wire_parts(), - ORDER_FULFILLMENT_UPDATE_CONTRACT_ID, - seller_pubkey.as_str(), + ORDER_REVISION_DECISION_CONTRACT_ID, + buyer_pubkey.as_str(), created_at_nostr, - "order fulfillment update", + "order revision decision", )?; - Ok(OrderFulfillmentUpdatePlan { + Ok(OrderRevisionDecisionPlan { workflow: order_workflow_plan( - OrderWorkflowKind::FulfillmentUpdate, + OrderWorkflowKind::RevisionDecision, expected_event_id.clone(), created_at, ), @@ -3175,41 +2282,42 @@ fn order_fulfillment_update_plan( } #[cfg(feature = "runtime")] -fn order_receipt_record_plan( +fn order_cancellation_plan( actor: &RadrootsActorContext, root_event: RadrootsNostrEventPtr, previous_event: RadrootsNostrEventPtr, - receipt: RadrootsOrderReceipt, + cancellation: RadrootsOrderCancellation, created_at: RadrootsSdkTimestamp, -) -> Result<OrderReceiptRecordPlan, RadrootsSdkError> { - require_buyer_actor(actor, "order.prepare_receipt_record")?; +) -> Result<OrderCancellationPlan, RadrootsSdkError> { + require_buyer_actor(actor, "order.prepare_cancellation")?; let root_event_id = order_reference_event_id(&root_event, "root")?; let previous_event_id = order_reference_event_id(&previous_event, "previous")?; - if receipt.buyer_pubkey.as_str() != actor.pubkey().as_str() { + if cancellation.buyer_pubkey.as_str() != actor.pubkey().as_str() { return Err(RadrootsSdkError::UnauthorizedActor { - operation: "order.prepare_receipt_record".to_owned(), + operation: "order.prepare_cancellation".to_owned(), reason: "actor pubkey must match order buyer_pubkey".to_owned(), }); } let created_at_nostr = created_at.try_into_nostr_created_at()?; - let order_id = receipt.order_id.clone(); - let listing_addr = receipt.listing_addr.clone(); - let buyer_pubkey = receipt.buyer_pubkey.clone(); - let seller_pubkey = receipt.seller_pubkey.clone(); - let draft = order::build_buyer_receipt_draft(&root_event_id, &previous_event_id, &receipt) - .map_err(|error| RadrootsSdkError::InvalidRequest { - message: format!("order receipt record draft encode failed: {error}"), - })?; + let order_id = cancellation.order_id.clone(); + let listing_addr = cancellation.listing_addr.clone(); + let buyer_pubkey = cancellation.buyer_pubkey.clone(); + let seller_pubkey = cancellation.seller_pubkey.clone(); + let draft = + order::build_order_cancellation_draft(&root_event_id, &previous_event_id, &cancellation) + .map_err(|error| RadrootsSdkError::InvalidRequest { + message: format!("order cancellation draft encode failed: {error}"), + })?; let (frozen_draft, expected_event_id) = freeze_order_workflow_draft( draft.into_wire_parts(), - ORDER_RECEIPT_CONTRACT_ID, + ORDER_CANCELLATION_CONTRACT_ID, buyer_pubkey.as_str(), created_at_nostr, - "order receipt record", + "order cancellation", )?; - Ok(OrderReceiptRecordPlan { + Ok(OrderCancellationPlan { workflow: order_workflow_plan( - OrderWorkflowKind::ReceiptRecord, + OrderWorkflowKind::Cancellation, expected_event_id.clone(), created_at, ), @@ -3384,6 +2492,14 @@ fn require_decision_request_evidence( ), }); } + if let Some(pending_revision_event_id) = projection.pending_revision_event_id.as_ref() { + return Err(RadrootsSdkError::InvalidRequest { + message: format!( + "order decision for order {} cannot follow pending revision proposal {}", + plan.order_id, pending_revision_event_id + ), + }); + } if !projection.issues.is_empty() { return Err(RadrootsSdkError::InvalidRequest { message: format!( @@ -3444,17 +2560,9 @@ fn require_revision_proposal_state( previous_event_id: &plan.previous_event_id, }; require_clean_lifecycle_projection(refs, projection)?; - require_lifecycle_status(&refs, projection, RadrootsOrderStatus::Accepted)?; + require_lifecycle_status(&refs, projection, RadrootsOrderStatus::Requested)?; require_no_lifecycle_terminal(&refs, projection)?; - require_no_payment_for_revision(&refs, projection)?; require_no_pending_revision(&refs, projection)?; - if projection.fulfillment_event_id.is_some() { - return Err(lifecycle_invalid( - refs.operation, - refs.order_id, - "revision proposal requires order before fulfillment", - )); - } require_lifecycle_previous_is_current(&refs, projection) } @@ -3473,7 +2581,7 @@ fn require_revision_decision_state( previous_event_id: &plan.previous_event_id, }; require_clean_lifecycle_projection(refs, projection)?; - require_lifecycle_status(&refs, projection, RadrootsOrderStatus::Accepted)?; + require_lifecycle_status(&refs, projection, RadrootsOrderStatus::Requested)?; require_no_lifecycle_terminal(&refs, projection)?; require_pending_revision(&refs, projection)?; require_lifecycle_previous_is_current(&refs, projection) @@ -3494,102 +2602,18 @@ fn require_cancellation_state( previous_event_id: &plan.previous_event_id, }; require_clean_lifecycle_projection(refs, projection)?; - if !matches!( - projection.status, - RadrootsOrderStatus::Requested | RadrootsOrderStatus::Accepted - ) { + if projection.status != RadrootsOrderStatus::Requested { return Err(lifecycle_invalid( refs.operation, refs.order_id, format!( - "cancellation requires requested or accepted local state; current state is {:?}", + "cancellation requires requested local state; current state is {:?}", projection.status ), )); } require_no_lifecycle_terminal(&refs, projection)?; require_no_pending_revision(&refs, projection)?; - if projection.fulfillment_event_id.is_some() { - return Err(lifecycle_invalid( - refs.operation, - refs.order_id, - "cancellation requires order before fulfillment", - )); - } - require_lifecycle_previous_is_current(&refs, projection) -} - -#[cfg(feature = "runtime")] -fn require_fulfillment_update_state( - plan: &OrderFulfillmentUpdatePlan, - projection: &RadrootsOrderProjection, -) -> Result<(), RadrootsSdkError> { - let refs = OrderLifecycleReferences { - operation: "order fulfillment update", - order_id: &plan.order_id, - listing_addr: &plan.listing_addr, - buyer_pubkey: &plan.buyer_pubkey, - seller_pubkey: &plan.seller_pubkey, - root_event_id: &plan.root_event_id, - previous_event_id: &plan.previous_event_id, - }; - require_clean_lifecycle_projection(refs, projection)?; - require_lifecycle_status(&refs, projection, RadrootsOrderStatus::Accepted)?; - require_no_lifecycle_terminal(&refs, projection)?; - require_no_pending_revision(&refs, projection)?; - if matches!( - projection.fulfillment_status, - Some( - RadrootsOrderFulfillmentState::Delivered - | RadrootsOrderFulfillmentState::SellerCancelled - ) - ) { - return Err(lifecycle_invalid( - refs.operation, - refs.order_id, - "fulfillment update cannot follow terminal fulfillment status", - )); - } - require_lifecycle_previous_is_current(&refs, projection) -} - -#[cfg(feature = "runtime")] -fn require_receipt_record_state( - plan: &OrderReceiptRecordPlan, - projection: &RadrootsOrderProjection, -) -> Result<(), RadrootsSdkError> { - let refs = OrderLifecycleReferences { - operation: "order receipt record", - order_id: &plan.order_id, - listing_addr: &plan.listing_addr, - buyer_pubkey: &plan.buyer_pubkey, - seller_pubkey: &plan.seller_pubkey, - root_event_id: &plan.root_event_id, - previous_event_id: &plan.previous_event_id, - }; - require_clean_lifecycle_projection(refs, projection)?; - require_lifecycle_status(&refs, projection, RadrootsOrderStatus::Accepted)?; - require_no_lifecycle_terminal(&refs, projection)?; - if projection.fulfillment_event_id.as_ref() != Some(refs.previous_event_id) { - return Err(lifecycle_invalid( - refs.operation, - refs.order_id, - "receipt record requires previous event to be the current fulfillment event", - )); - } - if !matches!( - projection.fulfillment_status, - Some( - RadrootsOrderFulfillmentState::ReadyForPickup - | RadrootsOrderFulfillmentState::Delivered - ) - ) { - return Err(lifecycle_invalid( - refs.operation, - refs.order_id, - "receipt record requires ready-for-pickup or delivered fulfillment state", - )); - } require_lifecycle_previous_is_current(&refs, projection) } @@ -3685,22 +2709,6 @@ fn require_no_lifecycle_terminal( } #[cfg(feature = "runtime")] -fn require_no_payment_for_revision( - refs: &OrderLifecycleReferences<'_>, - projection: &RadrootsOrderProjection, -) -> Result<(), RadrootsSdkError> { - if projection.payment.state == RadrootsOrderPaymentState::NotRecorded { - Ok(()) - } else { - Err(lifecycle_invalid( - refs.operation, - refs.order_id, - "revision proposal cannot follow recorded payment state", - )) - } -} - -#[cfg(feature = "runtime")] fn require_pending_revision( refs: &OrderLifecycleReferences<'_>, projection: &RadrootsOrderProjection, @@ -3923,54 +2931,12 @@ impl From<RadrootsOrderStatus> for OrderStatusKind { RadrootsOrderStatus::Accepted => Self::Accepted, RadrootsOrderStatus::Declined => Self::Declined, RadrootsOrderStatus::Cancelled => Self::Cancelled, - RadrootsOrderStatus::Completed => Self::Completed, - RadrootsOrderStatus::Disputed => Self::Disputed, RadrootsOrderStatus::Invalid => Self::Invalid, } } } #[cfg(feature = "runtime")] -impl From<RadrootsOrderFulfillmentState> for OrderFulfillmentStatusKind { - fn from(status: RadrootsOrderFulfillmentState) -> Self { - match status { - RadrootsOrderFulfillmentState::AcceptedNotFulfilled => Self::AcceptedNotFulfilled, - RadrootsOrderFulfillmentState::Preparing => Self::Preparing, - RadrootsOrderFulfillmentState::ReadyForPickup => Self::ReadyForPickup, - RadrootsOrderFulfillmentState::OutForDelivery => Self::OutForDelivery, - RadrootsOrderFulfillmentState::Delivered => Self::Delivered, - RadrootsOrderFulfillmentState::SellerCancelled => Self::SellerCancelled, - } - } -} - -#[cfg(feature = "runtime")] -impl From<RadrootsOrderPaymentState> for OrderPaymentStateKind { - fn from(state: RadrootsOrderPaymentState) -> Self { - match state { - RadrootsOrderPaymentState::NotRecorded => Self::NotRecorded, - RadrootsOrderPaymentState::Recorded => Self::Recorded, - RadrootsOrderPaymentState::Settled => Self::Settled, - RadrootsOrderPaymentState::Rejected => Self::Rejected, - RadrootsOrderPaymentState::Invalid => Self::Invalid, - } - } -} - -#[cfg(feature = "runtime")] -impl From<RadrootsOrderSettlementState> for OrderSettlementStateKind { - fn from(state: RadrootsOrderSettlementState) -> Self { - match state { - RadrootsOrderSettlementState::NotRequired => Self::NotRequired, - RadrootsOrderSettlementState::Pending => Self::Pending, - RadrootsOrderSettlementState::Accepted => Self::Accepted, - RadrootsOrderSettlementState::Rejected => Self::Rejected, - RadrootsOrderSettlementState::Invalid => Self::Invalid, - } - } -} - -#[cfg(feature = "runtime")] impl From<RadrootsOrderIssue> for SdkOrderStatusIssue { fn from(issue: RadrootsOrderIssue) -> Self { match issue { @@ -4043,12 +3009,6 @@ impl From<RadrootsOrderIssue> for SdkOrderStatusIssue { 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, @@ -4137,56 +3097,6 @@ impl From<RadrootsOrderIssue> for SdkOrderStatusIssue { 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, @@ -4229,177 +3139,6 @@ impl From<RadrootsOrderIssue> for SdkOrderStatusIssue { 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) } diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -13,9 +13,8 @@ use radroots_events::{ draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent}, ids::{RadrootsEventId, RadrootsOrderId}, kinds::{ - KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_FULFILLMENT_UPDATE, - KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST, KIND_ORDER_REVISION_DECISION, - KIND_ORDER_REVISION_PROPOSAL, + KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, + KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, }, }; use radroots_nostr::prelude::{ @@ -28,22 +27,18 @@ use radroots_sdk::protocol::events::RadrootsNostrEventPtr; use radroots_sdk::protocol::order::{ RadrootsListingAddress, RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, - RadrootsOrderEconomics, RadrootsOrderFulfillmentState, RadrootsOrderFulfillmentUpdate, - RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis, - RadrootsOrderReceipt, RadrootsOrderRequest, RadrootsOrderRevisionDecision, + RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem, + RadrootsOrderPricingBasis, RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, }; use radroots_sdk::protocol::wire::WireEventParts; use radroots_sdk::{ ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, - ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, ORDER_SUBMIT_OPERATION_KIND, OrderCancellationEnqueueRequest, OrderDecisionEnqueueRequest, OrderDecisionPrepareRequest, - OrderEvidenceIngestRequest, OrderFulfillmentStatusKind, OrderFulfillmentUpdateEnqueueRequest, - OrderPaymentHandoffKind, OrderPaymentStateKind, OrderReceiptRecordEnqueueRequest, - OrderRequestEvidenceIngestRequest, OrderRevisionDecisionEnqueueRequest, - OrderRevisionProposalEnqueueRequest, OrderSettlementStateKind, OrderStatusKind, + OrderEvidenceIngestRequest, OrderRequestEvidenceIngestRequest, + OrderRevisionDecisionEnqueueRequest, OrderRevisionProposalEnqueueRequest, OrderStatusKind, OrderStatusNextActionKind, OrderStatusRequest, OrderSubmitEnqueueRequest, OrderSubmitPrepareRequest, OrderWorkflowKind, PushOutboxEventState, PushOutboxRelayOutcomeKind, PushOutboxRequest, RadrootsSdk, RadrootsSdkError, RadrootsSdkPartialLocalMutationFailure, @@ -941,19 +936,6 @@ fn order_revision_decision( } } -fn order_fulfillment_update( - raw_order_id: &str, - status: RadrootsOrderFulfillmentState, -) -> RadrootsOrderFulfillmentUpdate { - RadrootsOrderFulfillmentUpdate { - order_id: order_id(raw_order_id), - listing_addr: listing_address(), - buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"), - seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"), - status, - } -} - fn order_cancellation(raw_order_id: &str) -> RadrootsOrderCancellation { RadrootsOrderCancellation { order_id: order_id(raw_order_id), @@ -964,22 +946,6 @@ fn order_cancellation(raw_order_id: &str) -> RadrootsOrderCancellation { } } -fn order_receipt_record(raw_order_id: &str, received: bool) -> RadrootsOrderReceipt { - RadrootsOrderReceipt { - order_id: order_id(raw_order_id), - listing_addr: listing_address(), - buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"), - seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"), - received, - issue: if received { - None - } else { - Some("missing one item".to_owned()) - }, - received_at: 1_785_000_000, - } -} - fn revision_economics() -> RadrootsOrderEconomics { RadrootsOrderEconomics { quote_id: "revision-quote-1".parse().expect("revision quote id"), @@ -1841,33 +1807,19 @@ async fn order_decision_enqueue_rejects_existing_decision_state_before_mutation( } #[tokio::test] -async fn order_revision_order_fulfillment_order_receipt_lifecycle_enqueue_updates_status() { +async fn order_revision_lifecycle_accepts_proposal_and_finalizes_agreement() { let (_tempdir, sdk, store) = directory_sdk_and_store().await; - let request_event = signed_order_request_event("order-lifecycle-complete", 50); + let request_event = signed_order_request_event("order-lifecycle-agreement", 50); let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id"); store .ingest_event(RadrootsEventIngest::new(request_event.clone(), 5_000)) .await .expect("ingest request"); - let decision_receipt = sdk - .orders() - .enqueue_decision( - OrderDecisionEnqueueRequest::new( - seller_actor(), - request_event_ptr(&request_event), - order_decision("order-lifecycle-complete"), - SdkRelayTargetPolicy::UseConfiguredRelays, - ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("decision target relays"), - &FixtureSigner::new(SELLER_SECRET_KEY_HEX), - ) - .await - .expect("enqueue decision"); + let proposal = order_revision_proposal( - "order-lifecycle-complete", + "order-lifecycle-agreement", + &request_event_id, &request_event_id, - &decision_receipt.signed_event_id, ); let proposal_receipt = sdk .orders() @@ -1875,7 +1827,7 @@ async fn order_revision_order_fulfillment_order_receipt_lifecycle_enqueue_update OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), - order_event_ptr(&decision_receipt.signed_event_id), + request_event_ptr(&request_event), proposal.clone(), SdkRelayTargetPolicy::UseConfiguredRelays, ) @@ -1941,94 +1893,13 @@ async fn order_revision_order_fulfillment_order_receipt_lifecycle_enqueue_update KIND_ORDER_REVISION_DECISION ); - let fulfillment_receipt = sdk - .orders() - .enqueue_fulfillment_update( - OrderFulfillmentUpdateEnqueueRequest::new( - seller_actor(), - request_event_ptr(&request_event), - order_event_ptr(&revision_decision_receipt.signed_event_id), - order_fulfillment_update( - "order-lifecycle-complete", - RadrootsOrderFulfillmentState::ReadyForPickup, - ), - SdkRelayTargetPolicy::UseConfiguredRelays, - ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("fulfillment target relays"), - &FixtureSigner::new(SELLER_SECRET_KEY_HEX), - ) - .await - .expect("enqueue fulfillment"); - assert_eq!( - outbox_operation_kind(&sdk, fulfillment_receipt.outbox_operation_id).await, - ORDER_FULFILLMENT_UPDATE_OPERATION_KIND - ); - assert_eq!( - store - .get_event(fulfillment_receipt.signed_event_id.as_str()) - .await - .expect("fulfillment lookup") - .expect("fulfillment") - .kind, - KIND_ORDER_FULFILLMENT_UPDATE - ); - - let receipt = sdk - .orders() - .enqueue_receipt_record( - OrderReceiptRecordEnqueueRequest::new( - buyer_actor(), - request_event_ptr(&request_event), - order_event_ptr(&fulfillment_receipt.signed_event_id), - order_receipt_record("order-lifecycle-complete", true), - SdkRelayTargetPolicy::UseConfiguredRelays, - ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("receipt target relays"), - &FixtureSigner::new(BUYER_SECRET_KEY_HEX), - ) - .await - .expect("enqueue receipt"); - assert_eq!( - outbox_operation_kind(&sdk, receipt.outbox_operation_id).await, - ORDER_RECEIPT_RECORD_OPERATION_KIND - ); - assert_eq!( - store - .get_event(receipt.signed_event_id.as_str()) - .await - .expect("receipt lookup") - .expect("receipt") - .kind, - KIND_ORDER_RECEIPT - ); - let status = sdk .orders() - .status(status_request("order-lifecycle-complete")) + .status(status_request("order-lifecycle-agreement")) .await .expect("status"); - assert_eq!(status.status, OrderStatusKind::Completed); - assert_eq!(status.event_count, 6); - assert_eq!( - status - .fulfillment_event_id - .as_ref() - .map(RadrootsEventId::as_str), - Some(fulfillment_receipt.signed_event_id.as_str()) - ); - assert_eq!( - status - .receipt_event_id - .as_ref() - .map(RadrootsEventId::as_str), - Some(receipt.signed_event_id.as_str()) - ); - assert_eq!( - status.fulfillment_status, - Some(OrderFulfillmentStatusKind::ReadyForPickup) - ); + assert_eq!(status.status, OrderStatusKind::Accepted); + assert_eq!(status.event_count, 3); assert_eq!( status .agreement_event_id @@ -2036,25 +1907,31 @@ async fn order_revision_order_fulfillment_order_receipt_lifecycle_enqueue_update .map(RadrootsEventId::as_str), Some(revision_decision_receipt.signed_event_id.as_str()) ); + assert!(status.decision_event_id.is_none()); + assert!(status.cancellation_event_id.is_none()); assert_eq!(status.pending_revision_event_id, None); - assert!(status.lifecycle_terminal); + assert_eq!(status.listing_addr, Some(listing_address())); assert_eq!( - status.payment_handoff, - OrderPaymentHandoffKind::InPersonOrOffPlatformPending + status.buyer_pubkey.as_ref().map(ToString::to_string), + Some(BUYER_PUBLIC_KEY_HEX.to_owned()) ); + assert_eq!( + status.seller_pubkey.as_ref().map(ToString::to_string), + Some(SELLER_PUBLIC_KEY_HEX.to_owned()) + ); + assert_eq!(status.economics, Some(revision_economics())); + assert!(status.lifecycle_terminal); assert_eq!(status.next_action, OrderStatusNextActionKind::Terminal); assert!(status.evidence.has_request); - assert!(status.evidence.has_decision); + assert!(!status.evidence.has_decision); assert!(status.evidence.has_agreement); - assert!(status.evidence.has_fulfillment); - assert!(status.evidence.has_receipt); + assert!(!status.evidence.has_pending_revision); + assert!(!status.evidence.has_cancellation); assert!(!status.evidence.has_issues); assert!(!status.eligibility.can_decide); assert!(!status.eligibility.can_propose_revision); assert!(!status.eligibility.can_decide_revision); assert!(!status.eligibility.can_cancel); - assert!(!status.eligibility.can_update_fulfillment); - assert!(!status.eligibility.can_record_receipt); assert!(status.issues.is_empty()); } @@ -2067,25 +1944,10 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif .ingest_event(RadrootsEventIngest::new(request_event.clone(), 5_500)) .await .expect("ingest request"); - let decision_receipt = sdk - .orders() - .enqueue_decision( - OrderDecisionEnqueueRequest::new( - seller_actor(), - request_event_ptr(&request_event), - order_decision("order-lifecycle-pending-revision"), - SdkRelayTargetPolicy::UseConfiguredRelays, - ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("decision target relays"), - &FixtureSigner::new(SELLER_SECRET_KEY_HEX), - ) - .await - .expect("enqueue decision"); let proposal = order_revision_proposal( "order-lifecycle-pending-revision", &request_event_id, - &decision_receipt.signed_event_id, + &request_event_id, ); let proposal_receipt = sdk .orders() @@ -2093,7 +1955,7 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), - order_event_ptr(&decision_receipt.signed_event_id), + request_event_ptr(&request_event), proposal, SdkRelayTargetPolicy::UseConfiguredRelays, ) @@ -2109,15 +1971,9 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif .status(status_request("order-lifecycle-pending-revision")) .await .expect("status"); - assert_eq!(status.status, OrderStatusKind::Accepted); - assert_eq!(status.event_count, 3); - assert_eq!( - status - .agreement_event_id - .as_ref() - .map(RadrootsEventId::as_str), - Some(decision_receipt.signed_event_id.as_str()) - ); + assert_eq!(status.status, OrderStatusKind::Requested); + assert_eq!(status.event_count, 2); + assert!(status.agreement_event_id.is_none()); assert_eq!( status .pending_revision_event_id @@ -2130,28 +1986,28 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif Some(proposal_receipt.signed_event_id.as_str()) ); assert!(status.issues.is_empty()); + assert!(!status.eligibility.can_decide); + assert!(!status.eligibility.can_propose_revision); + assert!(status.eligibility.can_decide_revision); + assert!(!status.eligibility.can_cancel); - let fulfillment_error = sdk + let decision_error = sdk .orders() - .enqueue_fulfillment_update( - OrderFulfillmentUpdateEnqueueRequest::new( + .enqueue_decision( + OrderDecisionEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), - order_event_ptr(&proposal_receipt.signed_event_id), - order_fulfillment_update( - "order-lifecycle-pending-revision", - RadrootsOrderFulfillmentState::ReadyForPickup, - ), + order_decision("order-lifecycle-pending-revision"), SdkRelayTargetPolicy::UseConfiguredRelays, ) .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("fulfillment target relays"), + .expect("decision target relays"), &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) .await - .expect_err("pending revision blocks fulfillment"); + .expect_err("pending revision blocks direct decision"); assert!(matches!( - fulfillment_error, + decision_error, RadrootsSdkError::InvalidRequest { .. } )); @@ -2186,12 +2042,12 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif .await .expect("event store status") .total_events, - 3 + 2 ); } #[tokio::test] -async fn order_declined_revision_clears_pending_and_allows_follow_on_lifecycle() { +async fn order_declined_revision_finalizes_declined_negotiation() { let (_tempdir, sdk, store) = directory_sdk_and_store().await; let request_event = signed_order_request_event("order-lifecycle-declined-revision", 56); let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id"); @@ -2199,25 +2055,10 @@ async fn order_declined_revision_clears_pending_and_allows_follow_on_lifecycle() .ingest_event(RadrootsEventIngest::new(request_event.clone(), 5_600)) .await .expect("ingest request"); - let decision_receipt = sdk - .orders() - .enqueue_decision( - OrderDecisionEnqueueRequest::new( - seller_actor(), - request_event_ptr(&request_event), - order_decision("order-lifecycle-declined-revision"), - SdkRelayTargetPolicy::UseConfiguredRelays, - ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("decision target relays"), - &FixtureSigner::new(SELLER_SECRET_KEY_HEX), - ) - .await - .expect("enqueue decision"); let proposal = order_revision_proposal( "order-lifecycle-declined-revision", &request_event_id, - &decision_receipt.signed_event_id, + &request_event_id, ); let proposal_receipt = sdk .orders() @@ -2225,7 +2066,7 @@ async fn order_declined_revision_clears_pending_and_allows_follow_on_lifecycle() OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), - order_event_ptr(&decision_receipt.signed_event_id), + request_event_ptr(&request_event), proposal.clone(), SdkRelayTargetPolicy::UseConfiguredRelays, ) @@ -2264,20 +2105,26 @@ async fn order_declined_revision_clears_pending_and_allows_follow_on_lifecycle() .status(status_request("order-lifecycle-declined-revision")) .await .expect("status"); - assert_eq!(status.status, OrderStatusKind::Accepted); - assert_eq!(status.event_count, 4); + assert_eq!(status.status, OrderStatusKind::Declined); + assert_eq!(status.event_count, 3); + assert!(status.agreement_event_id.is_none()); assert_eq!( status - .agreement_event_id + .pending_revision_event_id .as_ref() .map(RadrootsEventId::as_str), - Some(decision_receipt.signed_event_id.as_str()) + Some(proposal_receipt.signed_event_id.as_str()) ); - assert_eq!(status.pending_revision_event_id, None); assert_eq!( status.last_event_id.as_ref().map(RadrootsEventId::as_str), Some(declined_revision_receipt.signed_event_id.as_str()) ); + assert!(status.lifecycle_terminal); + assert_eq!(status.next_action, OrderStatusNextActionKind::Terminal); + assert!(!status.eligibility.can_decide); + assert!(!status.eligibility.can_propose_revision); + assert!(!status.eligibility.can_decide_revision); + assert!(!status.eligibility.can_cancel); let second_decision = order_revision_decision( &proposal, @@ -2310,60 +2157,8 @@ async fn order_declined_revision_clears_pending_and_allows_follow_on_lifecycle() .await .expect("event store status") .total_events, - 4 - ); - - let fulfillment_receipt = sdk - .orders() - .enqueue_fulfillment_update( - OrderFulfillmentUpdateEnqueueRequest::new( - seller_actor(), - request_event_ptr(&request_event), - order_event_ptr(&declined_revision_receipt.signed_event_id), - order_fulfillment_update( - "order-lifecycle-declined-revision", - RadrootsOrderFulfillmentState::ReadyForPickup, - ), - SdkRelayTargetPolicy::UseConfiguredRelays, - ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("fulfillment target relays"), - &FixtureSigner::new(SELLER_SECRET_KEY_HEX), - ) - .await - .expect("enqueue fulfillment"); - - let status = sdk - .orders() - .status(status_request("order-lifecycle-declined-revision")) - .await - .expect("status"); - assert_eq!(status.status, OrderStatusKind::Accepted); - assert_eq!(status.event_count, 5); - assert_eq!( - status - .agreement_event_id - .as_ref() - .map(RadrootsEventId::as_str), - Some(decision_receipt.signed_event_id.as_str()) - ); - assert_eq!(status.pending_revision_event_id, None); - assert_eq!( - status - .fulfillment_event_id - .as_ref() - .map(RadrootsEventId::as_str), - Some(fulfillment_receipt.signed_event_id.as_str()) - ); - assert_eq!( - status.last_event_id.as_ref().map(RadrootsEventId::as_str), - Some(fulfillment_receipt.signed_event_id.as_str()) - ); - assert_eq!( - status.fulfillment_status, - Some(OrderFulfillmentStatusKind::ReadyForPickup) + 3 ); - assert!(status.issues.is_empty()); } #[tokio::test] @@ -2375,28 +2170,13 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { .ingest_event(RadrootsEventIngest::new(request_event.clone(), 6_000)) .await .expect("ingest request"); - let decision_receipt = sdk - .orders() - .enqueue_decision( - OrderDecisionEnqueueRequest::new( - seller_actor(), - request_event_ptr(&request_event), - order_decision("order-lifecycle-cancel"), - SdkRelayTargetPolicy::UseConfiguredRelays, - ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("decision target relays"), - &FixtureSigner::new(SELLER_SECRET_KEY_HEX), - ) - .await - .expect("enqueue decision"); let cancellation = sdk .orders() .enqueue_cancellation( OrderCancellationEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), - order_event_ptr(&decision_receipt.signed_event_id), + request_event_ptr(&request_event), order_cancellation("order-lifecycle-cancel"), SdkRelayTargetPolicy::UseConfiguredRelays, ) @@ -2410,10 +2190,7 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { .expect("enqueue cancellation"); assert_eq!(cancellation.root_event_id, request_event_id); - assert_eq!( - cancellation.previous_event_id, - decision_receipt.signed_event_id - ); + assert_eq!(cancellation.previous_event_id, request_event_id); assert_eq!( outbox_operation_kind(&sdk, cancellation.outbox_operation_id).await, ORDER_CANCELLATION_OPERATION_KIND @@ -2433,7 +2210,7 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { OrderCancellationEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), - order_event_ptr(&decision_receipt.signed_event_id), + request_event_ptr(&request_event), order_cancellation("order-lifecycle-cancel"), SdkRelayTargetPolicy::UseConfiguredRelays, ) @@ -2462,14 +2239,12 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { Some(cancellation.signed_event_id.as_str()) ); assert!(status.lifecycle_terminal); - assert_eq!(status.payment_handoff, OrderPaymentHandoffKind::NotRequired); assert_eq!(status.next_action, OrderStatusNextActionKind::Terminal); assert!(status.evidence.has_request); - assert!(status.evidence.has_decision); + assert!(!status.evidence.has_decision); assert!(status.evidence.has_cancellation); assert!(!status.evidence.has_issues); assert!(!status.eligibility.can_cancel); - assert!(!status.eligibility.can_update_fulfillment); assert!(status.issues.is_empty()); } @@ -2480,14 +2255,15 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id"); let missing = sdk .orders() - .enqueue_fulfillment_update( - OrderFulfillmentUpdateEnqueueRequest::new( + .enqueue_revision_proposal( + OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), - order_event_ptr(&request_event_id), - order_fulfillment_update( + request_event_ptr(&request_event), + order_revision_proposal( "order-lifecycle-invalid", - RadrootsOrderFulfillmentState::ReadyForPickup, + &request_event_id, + &request_event_id, ), SdkRelayTargetPolicy::UseConfiguredRelays, ) @@ -2556,24 +2332,24 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { RadrootsSdkError::InvalidRequest { .. } )); - let receipt_error = sdk + let cancellation_error = sdk .orders() - .enqueue_receipt_record( - OrderReceiptRecordEnqueueRequest::new( + .enqueue_cancellation( + OrderCancellationEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), order_event_ptr(&decision_receipt.signed_event_id), - order_receipt_record("order-lifecycle-invalid", true), + order_cancellation("order-lifecycle-invalid"), SdkRelayTargetPolicy::UseConfiguredRelays, ) .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) - .expect("receipt target relays"), + .expect("cancellation target relays"), &FixtureSigner::new(BUYER_SECRET_KEY_HEX), ) .await - .expect_err("receipt without fulfillment"); + .expect_err("cancellation after accepted agreement"); assert!(matches!( - receipt_error, + cancellation_error, RadrootsSdkError::InvalidRequest { .. } )); assert_eq!( @@ -2602,12 +2378,10 @@ async fn order_status_returns_not_found_for_missing_local_order() { 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.payment_handoff, OrderPaymentHandoffKind::NotReady); + assert!(receipt.listing_addr.is_none()); + assert!(receipt.buyer_pubkey.is_none()); + assert!(receipt.seller_pubkey.is_none()); + assert!(receipt.economics.is_none()); assert_eq!(receipt.next_action, OrderStatusNextActionKind::NoLocalOrder); assert_eq!(receipt.evidence.event_count, 0); assert_eq!(receipt.evidence.limit_applied, ORDER_STATUS_DEFAULT_LIMIT); @@ -2615,7 +2389,8 @@ async fn order_status_returns_not_found_for_missing_local_order() { assert!(!receipt.evidence.has_issues); assert!(!receipt.eligibility.can_decide); assert!(!receipt.eligibility.can_cancel); - assert!(!receipt.eligibility.can_update_fulfillment); + assert!(!receipt.eligibility.can_propose_revision); + assert!(!receipt.eligibility.can_decide_revision); assert!(receipt.issues.is_empty()); } @@ -2678,15 +2453,18 @@ async fn order_status_contract_dtos_serialize_deterministically() { assert_eq!(receipt_json["source"], "local_event_store"); assert_eq!(receipt_json["status"], "missing"); - assert_eq!(receipt_json["payment_state"], "not_recorded"); - assert_eq!(receipt_json["settlement_state"], "not_required"); - assert_eq!(receipt_json["payment_handoff"], "not_ready"); + assert_eq!(receipt_json["listing_addr"], serde_json::Value::Null); + assert_eq!(receipt_json["buyer_pubkey"], serde_json::Value::Null); + assert_eq!(receipt_json["seller_pubkey"], serde_json::Value::Null); + assert_eq!(receipt_json["economics"], serde_json::Value::Null); assert_eq!(receipt_json["next_action"], "no_local_order"); assert_eq!(receipt_json["evidence"]["event_count"], 0); assert_eq!(receipt_json["evidence"]["limit_applied"], 25); assert_eq!(receipt_json["evidence"]["has_request"], false); assert_eq!(receipt_json["eligibility"]["can_decide"], false); assert_eq!(receipt_json["eligibility"]["can_cancel"], false); + assert_eq!(receipt_json["eligibility"]["can_propose_revision"], false); + assert_eq!(receipt_json["eligibility"]["can_decide_revision"], false); let issue = SdkOrderStatusIssue { kind: SdkOrderStatusIssueKind::DecisionPayloadInvalid, @@ -2758,16 +2536,19 @@ async fn order_status_projects_local_request_and_decision_events() { receipt.last_event_id.as_ref().map(RadrootsEventId::as_str), Some(decision_event.id.as_str()) ); - assert!(receipt.issues.is_empty()); - assert!(!receipt.lifecycle_terminal); + assert_eq!(receipt.listing_addr, Some(listing_address())); assert_eq!( - receipt.payment_handoff, - OrderPaymentHandoffKind::InPersonOrOffPlatformPending + receipt.buyer_pubkey.as_ref().map(ToString::to_string), + Some(BUYER_PUBLIC_KEY_HEX.to_owned()) ); assert_eq!( - receipt.next_action, - OrderStatusNextActionKind::ArrangeInPersonOrOffPlatformPayment + receipt.seller_pubkey.as_ref().map(ToString::to_string), + Some(SELLER_PUBLIC_KEY_HEX.to_owned()) ); + assert_eq!(receipt.economics, Some(economics())); + assert!(receipt.issues.is_empty()); + assert!(receipt.lifecycle_terminal); + assert_eq!(receipt.next_action, OrderStatusNextActionKind::Terminal); assert_eq!(receipt.evidence.event_count, 2); assert!(receipt.evidence.has_request); assert!(receipt.evidence.has_decision); @@ -2775,11 +2556,9 @@ async fn order_status_projects_local_request_and_decision_events() { assert!(!receipt.evidence.has_pending_revision); assert!(!receipt.evidence.has_issues); assert!(!receipt.eligibility.can_decide); - assert!(receipt.eligibility.can_propose_revision); + assert!(!receipt.eligibility.can_propose_revision); assert!(!receipt.eligibility.can_decide_revision); - assert!(receipt.eligibility.can_cancel); - assert!(receipt.eligibility.can_update_fulfillment); - assert!(!receipt.eligibility.can_record_receipt); + assert!(!receipt.eligibility.can_cancel); } #[tokio::test] diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -42,8 +42,6 @@ const FORBIDDEN_SDK_SOURCE_CONCEPTS: &[ForbiddenSdkConcept] = &[ const REQUIRED_ORDER_RUNTIME_EXPORTS: &[&str] = &[ "ORDER_CANCELLATION_OPERATION_KIND", "ORDER_DECISION_OPERATION_KIND", - "ORDER_FULFILLMENT_UPDATE_OPERATION_KIND", - "ORDER_RECEIPT_RECORD_OPERATION_KIND", "ORDER_REVISION_DECISION_OPERATION_KIND", "ORDER_REVISION_PROPOSAL_OPERATION_KIND", "ORDER_STATUS_DEFAULT_LIMIT", @@ -59,17 +57,6 @@ const REQUIRED_ORDER_RUNTIME_EXPORTS: &[&str] = &[ "OrderDecisionReceipt", "OrderEvidenceIngestReceipt", "OrderEvidenceIngestRequest", - "OrderFulfillmentStatusKind", - "OrderFulfillmentUpdateEnqueueRequest", - "OrderFulfillmentUpdatePlan", - "OrderFulfillmentUpdatePrepareRequest", - "OrderFulfillmentUpdateReceipt", - "OrderPaymentHandoffKind", - "OrderPaymentStateKind", - "OrderReceiptRecordEnqueueRequest", - "OrderReceiptRecordPlan", - "OrderReceiptRecordPrepareRequest", - "OrderReceiptRecordReceipt", "OrderRequestEvidenceIngestReceipt", "OrderRequestEvidenceIngestRequest", "OrderRevisionDecisionEnqueueRequest", @@ -80,7 +67,6 @@ const REQUIRED_ORDER_RUNTIME_EXPORTS: &[&str] = &[ "OrderRevisionProposalPlan", "OrderRevisionProposalPrepareRequest", "OrderRevisionProposalReceipt", - "OrderSettlementStateKind", "OrderStatusEligibility", "OrderStatusEvidenceSummary", "OrderStatusKind", @@ -119,43 +105,57 @@ const REQUIRED_ORDERS_CLIENT_METHODS: &[&str] = &[ "pub fn prepare_cancellation(", "pub async fn enqueue_cancellation<", "pub async fn enqueue_prepared_cancellation<", - "pub fn prepare_fulfillment_update(", - "pub async fn enqueue_fulfillment_update<", - "pub async fn enqueue_prepared_fulfillment_update<", - "pub fn prepare_receipt_record(", - "pub async fn enqueue_receipt_record<", - "pub async fn enqueue_prepared_receipt_record<", "pub async fn status(", ]; -const FORBIDDEN_PAYMENT_WRITE_PUBLIC_EXPORTS: &[&str] = &[ +const FORBIDDEN_ORDER_RUNTIME_PUBLIC_EXPORTS: &[&str] = &[ "CheckoutClient", "EscrowClient", "InvoiceClient", - "OrderPaymentRecordEnqueueRequest", - "OrderPaymentRecordPrepareRequest", - "OrderPaymentRecordReceipt", - "OrderSettlementDecisionEnqueueRequest", - "OrderSettlementDecisionPrepareRequest", - "OrderSettlementDecisionReceipt", + concat!("Order", "FulfillmentStatusKind"), + concat!("Order", "FulfillmentUpdateEnqueueRequest"), + concat!("Order", "FulfillmentUpdatePlan"), + concat!("Order", "FulfillmentUpdatePrepareRequest"), + concat!("Order", "FulfillmentUpdateReceipt"), + concat!("Order", "Payment", "HandoffKind"), + concat!("Order", "PaymentRecordEnqueueRequest"), + concat!("Order", "PaymentRecordPrepareRequest"), + concat!("Order", "Payment", "Record", "Receipt"), + concat!("Order", "Payment", "StateKind"), + concat!("Order", "ReceiptRecordEnqueueRequest"), + concat!("Order", "ReceiptRecordPlan"), + concat!("Order", "ReceiptRecordPrepareRequest"), + concat!("Order", "ReceiptRecord", "Receipt"), + concat!("Order", "SettlementDecisionEnqueueRequest"), + concat!("Order", "SettlementDecisionPrepareRequest"), + concat!("Order", "SettlementDecisionReceipt"), + concat!("Order", "Settlement", "StateKind"), "PaymentClient", "PaymentsClient", "RefundClient", "WalletClient", + "ORDER_FULFILLMENT_UPDATE_OPERATION_KIND", "ORDER_PAYMENT_RECORD_OPERATION_KIND", + "ORDER_RECEIPT_RECORD_OPERATION_KIND", "ORDER_SETTLEMENT_DECISION_OPERATION_KIND", ]; -const FORBIDDEN_PAYMENT_WRITE_ORDER_METHODS: &[&str] = &[ +const FORBIDDEN_ORDER_RUNTIME_METHODS: &[&str] = &[ "accept_settlement", "checkout", + "enqueue_fulfillment", "enqueue_payment", + "enqueue_receipt_record", "enqueue_settlement", "escrow", + "fulfillment", "invoice", "payment_provider", + "prepare_fulfillment", "prepare_payment", + "prepare_receipt_record", "prepare_settlement", + "receipt_record", "record_payment", "refund", "reject_settlement", @@ -210,7 +210,7 @@ fn migrated_runtime_tests_stay_on_product_runtime_boundary() { } #[test] -fn payment_deferral_keeps_sdk_public_runtime_surface_passive_only() { +fn agreement_order_runtime_excludes_post_agreement_surfaces() { let lib_source = read_source( Path::new(env!("CARGO_MANIFEST_DIR")) .join("src/lib.rs") @@ -222,28 +222,17 @@ fn payment_deferral_keeps_sdk_public_runtime_surface_passive_only() { .as_path(), ); - for passive_export in [ - "OrderPaymentHandoffKind", - "OrderPaymentStateKind", - "OrderSettlementStateKind", - ] { - assert!( - lib_source.contains(passive_export), - "src/lib.rs must keep passive order payment status export `{passive_export}`" - ); - } - - for forbidden in FORBIDDEN_PAYMENT_WRITE_PUBLIC_EXPORTS { + for forbidden in FORBIDDEN_ORDER_RUNTIME_PUBLIC_EXPORTS { assert!( !lib_source.contains(forbidden), - "src/lib.rs must not expose deferred payment write surface `{forbidden}`" + "src/lib.rs must not expose unsupported order runtime surface `{forbidden}`" ); } - for forbidden in FORBIDDEN_PAYMENT_WRITE_ORDER_METHODS { + for forbidden in FORBIDDEN_ORDER_RUNTIME_METHODS { assert!( !order_source.contains(forbidden), - "src/orders_runtime.rs must not add deferred payment write method or capability `{forbidden}`" + "src/orders_runtime.rs must not expose unsupported order runtime method or capability `{forbidden}`" ); } }