commit fd8fca74c6b3d85d143f7cacb4fbe780745455f3
parent 329e76d4596b0333d10265e02382868667104042
Author: triesap <tyson@radroots.org>
Date: Fri, 19 Jun 2026 17:57:48 -0700
trade: remove post-agreement order events
- Drop payment, settlement, receipt, and fulfillment order event contracts.
- Keep request, decision, revision, and cancellation reducers agreement-only.
- Preserve validation receipts as technical proof infrastructure.
- Validate event, codec, trade, event-store, and contract gates.
Diffstat:
9 files changed, 1117 insertions(+), 7222 deletions(-)
diff --git a/crates/events/src/contract.rs b/crates/events/src/contract.rs
@@ -1276,38 +1276,6 @@ static ALL_KIND_CONTRACTS: &[RadrootsKindContract] = &[
["radroots.order.cancellation.v1"]
),
kind_contract!(
- KIND_ORDER_FULFILLMENT_UPDATE,
- "KIND_ORDER_FULFILLMENT_UPDATE",
- "Order Fulfillment Update",
- RadrootsEventClass::Regular,
- RadrootsNostrStandard::Radroots,
- ["radroots.order.fulfillment_update.v1"]
- ),
- kind_contract!(
- KIND_ORDER_RECEIPT,
- "KIND_ORDER_RECEIPT",
- "Order Receipt",
- RadrootsEventClass::Regular,
- RadrootsNostrStandard::Radroots,
- ["radroots.order.receipt.v1"]
- ),
- kind_contract!(
- KIND_ORDER_PAYMENT_RECORD,
- "KIND_ORDER_PAYMENT_RECORD",
- "Order Payment Record",
- RadrootsEventClass::Regular,
- RadrootsNostrStandard::Radroots,
- ["radroots.order.payment_record.v1"]
- ),
- kind_contract!(
- KIND_ORDER_SETTLEMENT_DECISION,
- "KIND_ORDER_SETTLEMENT_DECISION",
- "Order Settlement Decision",
- RadrootsEventClass::Regular,
- RadrootsNostrStandard::Radroots,
- ["radroots.order.settlement_decision.v1"]
- ),
- kind_contract!(
KIND_TRADE_VALIDATION_RECEIPT,
"KIND_TRADE_VALIDATION_RECEIPT",
"Trade Validation Receipt",
@@ -2411,58 +2379,6 @@ static ALL_EVENT_CONTRACTS: &[RadrootsEventContract] = &[
ORDER_REDUCERS
),
event_contract!(
- "radroots.order.fulfillment_update.v1",
- KIND_ORDER_FULFILLMENT_UPDATE,
- "Order Fulfillment Update",
- "RadrootsOrderFulfillmentUpdate",
- RadrootsEventClass::Regular,
- RadrootsEventPrivacy::Public,
- RadrootsActorRole::Seller,
- RadrootsContentSchema::JsonObject,
- RadrootsEventDiscriminator::KindOnly,
- CHAINED_ORDER_TAGS,
- ORDER_REDUCERS
- ),
- event_contract!(
- "radroots.order.receipt.v1",
- KIND_ORDER_RECEIPT,
- "Order Receipt",
- "RadrootsOrderReceipt",
- RadrootsEventClass::Regular,
- RadrootsEventPrivacy::Public,
- RadrootsActorRole::Buyer,
- RadrootsContentSchema::JsonObject,
- RadrootsEventDiscriminator::KindOnly,
- CHAINED_ORDER_TAGS,
- ORDER_REDUCERS
- ),
- event_contract!(
- "radroots.order.payment_record.v1",
- KIND_ORDER_PAYMENT_RECORD,
- "Order Payment Record",
- "RadrootsOrderPaymentRecord",
- RadrootsEventClass::Regular,
- RadrootsEventPrivacy::Public,
- RadrootsActorRole::Buyer,
- RadrootsContentSchema::JsonObject,
- RadrootsEventDiscriminator::KindOnly,
- CHAINED_ORDER_TAGS,
- ORDER_REDUCERS
- ),
- event_contract!(
- "radroots.order.settlement_decision.v1",
- KIND_ORDER_SETTLEMENT_DECISION,
- "Order Settlement Decision",
- "RadrootsOrderSettlementDecision",
- RadrootsEventClass::Regular,
- RadrootsEventPrivacy::Public,
- RadrootsActorRole::Seller,
- RadrootsContentSchema::JsonObject,
- RadrootsEventDiscriminator::KindOnly,
- CHAINED_ORDER_TAGS,
- ORDER_REDUCERS
- ),
- event_contract!(
"radroots.trade.validation_receipt.v1",
KIND_TRADE_VALIDATION_RECEIPT,
"Trade Validation Receipt",
diff --git a/crates/events/src/kinds.rs b/crates/events/src/kinds.rs
@@ -89,24 +89,16 @@ pub const KIND_ORDER_DECISION: u32 = 3423;
pub const KIND_ORDER_REVISION_PROPOSAL: u32 = 3424;
pub const KIND_ORDER_REVISION_DECISION: u32 = 3425;
pub const KIND_ORDER_CANCELLATION: u32 = 3432;
-pub const KIND_ORDER_FULFILLMENT_UPDATE: u32 = 3433;
-pub const KIND_ORDER_RECEIPT: u32 = 3434;
-pub const KIND_ORDER_PAYMENT_RECORD: u32 = 3435;
-pub const KIND_ORDER_SETTLEMENT_DECISION: u32 = 3436;
pub const KIND_TRADE_VALIDATION_RECEIPT: u32 = 3440;
pub const LISTING_EVENT_KINDS: [u32; 2] = [KIND_LISTING, KIND_LISTING_DRAFT];
-pub const ORDER_EVENT_KINDS: [u32; 9] = [
+pub const ORDER_EVENT_KINDS: [u32; 5] = [
KIND_ORDER_REQUEST,
KIND_ORDER_DECISION,
KIND_ORDER_REVISION_PROPOSAL,
KIND_ORDER_REVISION_DECISION,
KIND_ORDER_CANCELLATION,
- KIND_ORDER_FULFILLMENT_UPDATE,
- KIND_ORDER_RECEIPT,
- KIND_ORDER_PAYMENT_RECORD,
- KIND_ORDER_SETTLEMENT_DECISION,
];
pub const TRADE_VALIDATION_SERVICE_EVENT_KINDS: [u32; 4] = [
@@ -124,7 +116,7 @@ pub const TRADE_VALIDATION_EVENT_KINDS: [u32; 5] = [
KIND_TRADE_VALIDATION_RECEIPT,
];
-pub const COMMERCIAL_EVENT_KINDS: [u32; 16] = [
+pub const COMMERCIAL_EVENT_KINDS: [u32; 12] = [
KIND_LISTING,
KIND_LISTING_DRAFT,
KIND_ORDER_REQUEST,
@@ -132,10 +124,6 @@ pub const COMMERCIAL_EVENT_KINDS: [u32; 16] = [
KIND_ORDER_REVISION_PROPOSAL,
KIND_ORDER_REVISION_DECISION,
KIND_ORDER_CANCELLATION,
- KIND_ORDER_FULFILLMENT_UPDATE,
- KIND_ORDER_RECEIPT,
- KIND_ORDER_PAYMENT_RECORD,
- KIND_ORDER_SETTLEMENT_DECISION,
KIND_TRADE_LISTING_VALIDATION_REQUEST,
KIND_TRADE_LISTING_VALIDATION_RESULT,
KIND_TRADE_TRANSITION_PROOF_REQUEST,
@@ -463,10 +451,6 @@ pub const fn is_order_event_kind(kind: u32) -> bool {
| KIND_ORDER_REVISION_PROPOSAL
| KIND_ORDER_REVISION_DECISION
| KIND_ORDER_CANCELLATION
- | KIND_ORDER_FULFILLMENT_UPDATE
- | KIND_ORDER_RECEIPT
- | KIND_ORDER_PAYMENT_RECORD
- | KIND_ORDER_SETTLEMENT_DECISION
)
}
@@ -791,10 +775,6 @@ mod tests {
KIND_ORDER_REVISION_PROPOSAL,
KIND_ORDER_REVISION_DECISION,
KIND_ORDER_CANCELLATION,
- KIND_ORDER_FULFILLMENT_UPDATE,
- KIND_ORDER_RECEIPT,
- KIND_ORDER_PAYMENT_RECORD,
- KIND_ORDER_SETTLEMENT_DECISION,
]
);
assert_eq!(
@@ -816,7 +796,7 @@ mod tests {
KIND_TRADE_VALIDATION_RECEIPT,
]
);
- assert_eq!(COMMERCIAL_EVENT_KINDS.len(), 16);
+ assert_eq!(COMMERCIAL_EVENT_KINDS.len(), 12);
assert!(is_listing_event_kind(KIND_LISTING));
assert!(is_listing_event_kind(KIND_LISTING_DRAFT));
@@ -827,10 +807,10 @@ mod tests {
assert!(is_order_event_kind(KIND_ORDER_REVISION_PROPOSAL));
assert!(is_order_event_kind(KIND_ORDER_REVISION_DECISION));
assert!(is_order_event_kind(KIND_ORDER_CANCELLATION));
- assert!(is_order_event_kind(KIND_ORDER_FULFILLMENT_UPDATE));
- assert!(is_order_event_kind(KIND_ORDER_RECEIPT));
- assert!(is_order_event_kind(KIND_ORDER_PAYMENT_RECORD));
- assert!(is_order_event_kind(KIND_ORDER_SETTLEMENT_DECISION));
+ assert!(!is_order_event_kind(3433));
+ assert!(!is_order_event_kind(3434));
+ assert!(!is_order_event_kind(3435));
+ assert!(!is_order_event_kind(3436));
assert!(!is_order_event_kind(KIND_TRADE_LISTING_VALIDATION_REQUEST));
assert!(!is_order_event_kind(KIND_TRADE_VALIDATION_RECEIPT));
assert!(!is_order_event_kind(3431));
@@ -869,14 +849,14 @@ mod tests {
assert!(is_trade_validation_receipt_kind(
KIND_TRADE_VALIDATION_RECEIPT
));
- assert!(!is_trade_validation_receipt_kind(KIND_ORDER_RECEIPT));
+ assert!(!is_trade_validation_receipt_kind(3434));
assert!(is_trade_validation_event_kind(
KIND_TRADE_VALIDATION_RECEIPT
));
assert!(is_trade_validation_event_kind(
KIND_TRADE_TRANSITION_PROOF_RESULT
));
- assert!(!is_trade_validation_event_kind(KIND_ORDER_RECEIPT));
+ assert!(!is_trade_validation_event_kind(3434));
assert!(is_commercial_event_kind(KIND_LISTING));
assert!(is_commercial_event_kind(KIND_ORDER_REQUEST));
diff --git a/crates/events/src/order.rs b/crates/events/src/order.rs
@@ -6,9 +6,11 @@ use alloc::{
vec::Vec,
};
+#[cfg(test)]
+use crate::ids::RadrootsOrderQuoteId;
use crate::ids::{
- RadrootsEconomicsDigest, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress,
- RadrootsOrderId, RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey,
+ RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
+ RadrootsOrderRevisionId, RadrootsPublicKey,
};
use crate::kinds::*;
pub use crate::order_economics::*;
@@ -307,49 +309,6 @@ impl RadrootsOrderDecision {
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum RadrootsOrderFulfillmentState {
- AcceptedNotFulfilled,
- Preparing,
- ReadyForPickup,
- OutForDelivery,
- Delivered,
- SellerCancelled,
-}
-
-impl RadrootsOrderFulfillmentState {
- #[inline]
- pub const fn is_publishable_update(self) -> bool {
- !matches!(self, Self::AcceptedNotFulfilled)
- }
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderFulfillmentUpdate {
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub status: RadrootsOrderFulfillmentState,
-}
-
-impl RadrootsOrderFulfillmentUpdate {
- pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> {
- validate_required_field(&self.order_id, "order_id")?;
- validate_required_field(&self.listing_addr, "listing_addr")?;
- validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?;
- validate_required_field(&self.seller_pubkey, "seller_pubkey")?;
- if self.status.is_publishable_update() {
- Ok(())
- } else {
- Err(RadrootsOrderPayloadError::InvalidFulfillmentStatus)
- }
- }
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsOrderCancellation {
pub order_id: RadrootsOrderId,
@@ -370,152 +329,6 @@ impl RadrootsOrderCancellation {
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderReceipt {
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub received: bool,
- pub issue: Option<String>,
- pub received_at: u64,
-}
-
-impl RadrootsOrderReceipt {
- pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> {
- validate_required_field(&self.order_id, "order_id")?;
- validate_required_field(&self.listing_addr, "listing_addr")?;
- validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?;
- validate_required_field(&self.seller_pubkey, "seller_pubkey")?;
- if self.received {
- if self.issue.is_some() {
- return Err(RadrootsOrderPayloadError::UnexpectedReceiptIssue);
- }
- } else {
- match self.issue.as_deref() {
- Some(issue) => validate_required_field(issue, "issue")?,
- None => return Err(RadrootsOrderPayloadError::MissingReceiptIssue),
- }
- }
- Ok(())
- }
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum RadrootsOrderPaymentMethod {
- Cash,
- ManualTransfer,
- Other,
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderPaymentRecord {
- 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 agreement_event_id: RadrootsEventId,
- pub quote_id: RadrootsOrderQuoteId,
- pub quote_version: u32,
- pub economics_digest: RadrootsEconomicsDigest,
- pub amount: RadrootsCoreDecimal,
- pub currency: RadrootsCoreCurrency,
- pub method: RadrootsOrderPaymentMethod,
- pub reference: Option<String>,
- pub paid_at: Option<u64>,
-}
-
-impl RadrootsOrderPaymentRecord {
- pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> {
- validate_required_field(&self.order_id, "order_id")?;
- validate_required_field(&self.listing_addr, "listing_addr")?;
- validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?;
- validate_required_field(&self.seller_pubkey, "seller_pubkey")?;
- validate_required_field(&self.root_event_id, "root_event_id")?;
- validate_required_field(&self.previous_event_id, "previous_event_id")?;
- validate_required_field(&self.agreement_event_id, "agreement_event_id")?;
- validate_required_field(&self.quote_id, "quote_id")?;
- validate_required_field(&self.economics_digest, "economics_digest")?;
- if self.quote_version == 0 {
- return Err(RadrootsOrderPayloadError::InvalidQuoteVersion);
- }
- if self.amount.is_zero() || self.amount.is_sign_negative() {
- return Err(RadrootsOrderPayloadError::InvalidPaymentAmount);
- }
- if let Some(reference) = self.reference.as_deref() {
- validate_required_field(reference, "reference")?;
- }
- Ok(())
- }
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum RadrootsOrderSettlementOutcome {
- Accepted,
- Rejected,
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderSettlementDecision {
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub seller_pubkey: RadrootsPublicKey,
- pub buyer_pubkey: RadrootsPublicKey,
- pub root_event_id: RadrootsEventId,
- pub previous_event_id: RadrootsEventId,
- pub agreement_event_id: RadrootsEventId,
- pub payment_event_id: RadrootsEventId,
- pub quote_id: RadrootsOrderQuoteId,
- pub quote_version: u32,
- pub economics_digest: RadrootsEconomicsDigest,
- pub amount: RadrootsCoreDecimal,
- pub currency: RadrootsCoreCurrency,
- pub decision: RadrootsOrderSettlementOutcome,
- pub reason: Option<String>,
-}
-
-impl RadrootsOrderSettlementDecision {
- pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> {
- validate_required_field(&self.order_id, "order_id")?;
- validate_required_field(&self.listing_addr, "listing_addr")?;
- validate_required_field(&self.seller_pubkey, "seller_pubkey")?;
- validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?;
- validate_required_field(&self.root_event_id, "root_event_id")?;
- validate_required_field(&self.previous_event_id, "previous_event_id")?;
- validate_required_field(&self.agreement_event_id, "agreement_event_id")?;
- validate_required_field(&self.payment_event_id, "payment_event_id")?;
- validate_required_field(&self.quote_id, "quote_id")?;
- validate_required_field(&self.economics_digest, "economics_digest")?;
- if self.quote_version == 0 {
- return Err(RadrootsOrderPayloadError::InvalidQuoteVersion);
- }
- if self.amount.is_zero() || self.amount.is_sign_negative() {
- return Err(RadrootsOrderPayloadError::InvalidPaymentAmount);
- }
- match self.decision {
- RadrootsOrderSettlementOutcome::Accepted => {
- if self.reason.is_some() {
- return Err(RadrootsOrderPayloadError::UnexpectedSettlementReason);
- }
- }
- RadrootsOrderSettlementOutcome::Rejected => match self.reason.as_deref() {
- Some(reason) => validate_required_field(reason, "reason")?,
- None => return Err(RadrootsOrderPayloadError::MissingSettlementReason),
- },
- }
- Ok(())
- }
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RadrootsCommercialDomain {
@@ -536,14 +349,6 @@ pub enum RadrootsOrderEventType {
OrderRevisionDecision,
#[cfg_attr(feature = "serde", serde(rename = "TradeOrderCancelled"))]
OrderCancelled,
- #[cfg_attr(feature = "serde", serde(rename = "TradeFulfillmentUpdated"))]
- FulfillmentUpdated,
- #[cfg_attr(feature = "serde", serde(rename = "TradeBuyerReceipt"))]
- BuyerReceipt,
- #[cfg_attr(feature = "serde", serde(rename = "TradePaymentRecorded"))]
- PaymentRecorded,
- #[cfg_attr(feature = "serde", serde(rename = "TradeSettlementDecision"))]
- SettlementDecision,
}
impl RadrootsOrderEventType {
@@ -555,10 +360,6 @@ impl RadrootsOrderEventType {
KIND_ORDER_REVISION_PROPOSAL => Some(Self::OrderRevisionProposed),
KIND_ORDER_REVISION_DECISION => Some(Self::OrderRevisionDecision),
KIND_ORDER_CANCELLATION => Some(Self::OrderCancelled),
- KIND_ORDER_FULFILLMENT_UPDATE => Some(Self::FulfillmentUpdated),
- KIND_ORDER_RECEIPT => Some(Self::BuyerReceipt),
- KIND_ORDER_PAYMENT_RECORD => Some(Self::PaymentRecorded),
- KIND_ORDER_SETTLEMENT_DECISION => Some(Self::SettlementDecision),
_ => None,
}
}
@@ -571,10 +372,6 @@ impl RadrootsOrderEventType {
Self::OrderRevisionProposed => KIND_ORDER_REVISION_PROPOSAL,
Self::OrderRevisionDecision => KIND_ORDER_REVISION_DECISION,
Self::OrderCancelled => KIND_ORDER_CANCELLATION,
- Self::FulfillmentUpdated => KIND_ORDER_FULFILLMENT_UPDATE,
- Self::BuyerReceipt => KIND_ORDER_RECEIPT,
- Self::PaymentRecorded => KIND_ORDER_PAYMENT_RECORD,
- Self::SettlementDecision => KIND_ORDER_SETTLEMENT_DECISION,
}
}
@@ -586,10 +383,6 @@ impl RadrootsOrderEventType {
Self::OrderRevisionProposed => "TradeOrderRevisionProposed",
Self::OrderRevisionDecision => "TradeOrderRevisionDecision",
Self::OrderCancelled => "TradeOrderCancelled",
- Self::FulfillmentUpdated => "TradeFulfillmentUpdated",
- Self::BuyerReceipt => "TradeBuyerReceipt",
- Self::PaymentRecorded => "TradePaymentRecorded",
- Self::SettlementDecision => "TradeSettlementDecision",
}
}
@@ -606,10 +399,6 @@ impl RadrootsOrderEventType {
| Self::OrderRevisionProposed
| Self::OrderRevisionDecision
| Self::OrderCancelled
- | Self::FulfillmentUpdated
- | Self::BuyerReceipt
- | Self::PaymentRecorded
- | Self::SettlementDecision
)
}
}
@@ -706,12 +495,6 @@ pub enum RadrootsOrderPayloadError {
InvalidQuoteVersion,
MissingInventoryCommitments,
InvalidInventoryCommitmentCount { index: usize },
- InvalidFulfillmentStatus,
- MissingReceiptIssue,
- UnexpectedReceiptIssue,
- InvalidPaymentAmount,
- MissingSettlementReason,
- UnexpectedSettlementReason,
}
impl core::fmt::Display for RadrootsOrderPayloadError {
@@ -777,27 +560,6 @@ impl core::fmt::Display for RadrootsOrderPayloadError {
f,
"inventory_commitments[{index}].bin_count must be greater than zero"
),
- Self::InvalidFulfillmentStatus => {
- write!(f, "fulfillment status is not publishable")
- }
- Self::MissingReceiptIssue => {
- write!(f, "receipt issue is required when received is false")
- }
- Self::UnexpectedReceiptIssue => {
- write!(f, "receipt issue must be absent when received is true")
- }
- Self::InvalidPaymentAmount => {
- write!(f, "payment amount must be greater than zero")
- }
- Self::MissingSettlementReason => {
- write!(f, "settlement reason is required when decision is rejected")
- }
- Self::UnexpectedSettlementReason => {
- write!(
- f,
- "settlement reason must be absent when decision is accepted"
- )
- }
}
}
}
@@ -1109,10 +871,6 @@ mod tests {
raw.parse().unwrap()
}
- fn digest(raw: &str) -> RadrootsEconomicsDigest {
- raw.parse().unwrap()
- }
-
fn sample_order_request() -> RadrootsOrderRequest {
RadrootsOrderRequest {
order_id: order_id("order-1"),
@@ -1237,16 +995,6 @@ mod tests {
}
}
- fn sample_order_fulfillment_update() -> RadrootsOrderFulfillmentUpdate {
- RadrootsOrderFulfillmentUpdate {
- order_id: order_id("order-1"),
- listing_addr: sample_listing_addr(),
- buyer_pubkey: buyer_pubkey(),
- seller_pubkey: seller_pubkey(),
- status: RadrootsOrderFulfillmentState::ReadyForPickup,
- }
- }
-
fn sample_order_cancellation() -> RadrootsOrderCancellation {
RadrootsOrderCancellation {
order_id: order_id("order-1"),
@@ -1257,18 +1005,6 @@ mod tests {
}
}
- fn sample_order_buyer_receipt(received: bool) -> RadrootsOrderReceipt {
- RadrootsOrderReceipt {
- order_id: order_id("order-1"),
- listing_addr: sample_listing_addr(),
- buyer_pubkey: buyer_pubkey(),
- seller_pubkey: seller_pubkey(),
- received,
- issue: (!received).then(|| "damaged items".into()),
- received_at: 1_777_665_600,
- }
- }
-
fn sample_order_revision_proposal() -> RadrootsOrderRevisionProposal {
RadrootsOrderRevisionProposal {
revision_id: revision_id("rev-1"),
@@ -1302,49 +1038,6 @@ mod tests {
}
}
- fn sample_payment_recorded() -> RadrootsOrderPaymentRecord {
- RadrootsOrderPaymentRecord {
- order_id: order_id("order-1"),
- listing_addr: sample_listing_addr(),
- buyer_pubkey: buyer_pubkey(),
- seller_pubkey: seller_pubkey(),
- root_event_id: event_id('1'),
- previous_event_id: event_id('2'),
- agreement_event_id: event_id('3'),
- quote_id: quote_id("quote-1"),
- quote_version: 1,
- economics_digest: digest("economics-digest"),
- amount: decimal("16"),
- currency: RadrootsCoreCurrency::USD,
- method: RadrootsOrderPaymentMethod::ManualTransfer,
- reference: Some("bank-ref".into()),
- paid_at: Some(1_777_665_600),
- }
- }
-
- fn sample_settlement_decision(
- decision: RadrootsOrderSettlementOutcome,
- reason: Option<&str>,
- ) -> RadrootsOrderSettlementDecision {
- RadrootsOrderSettlementDecision {
- order_id: order_id("order-1"),
- listing_addr: sample_listing_addr(),
- seller_pubkey: seller_pubkey(),
- buyer_pubkey: buyer_pubkey(),
- root_event_id: event_id('1'),
- previous_event_id: event_id('2'),
- agreement_event_id: event_id('3'),
- payment_event_id: event_id('4'),
- quote_id: quote_id("quote-1"),
- quote_version: 1,
- economics_digest: digest("economics-digest"),
- amount: decimal("16"),
- currency: RadrootsCoreCurrency::USD,
- decision,
- reason: reason.map(Into::into),
- }
- }
-
#[test]
fn order_message_type_uses_canonical_names_and_kinds() {
assert_eq!(
@@ -1364,25 +1057,13 @@ mod tests {
Some(RadrootsOrderEventType::OrderRevisionDecision)
);
assert_eq!(
- RadrootsOrderEventType::from_kind(KIND_ORDER_FULFILLMENT_UPDATE),
- Some(RadrootsOrderEventType::FulfillmentUpdated)
- );
- assert_eq!(
RadrootsOrderEventType::from_kind(KIND_ORDER_CANCELLATION),
Some(RadrootsOrderEventType::OrderCancelled)
);
- assert_eq!(
- RadrootsOrderEventType::from_kind(KIND_ORDER_RECEIPT),
- Some(RadrootsOrderEventType::BuyerReceipt)
- );
- assert_eq!(
- RadrootsOrderEventType::from_kind(KIND_ORDER_PAYMENT_RECORD),
- Some(RadrootsOrderEventType::PaymentRecorded)
- );
- assert_eq!(
- RadrootsOrderEventType::from_kind(KIND_ORDER_SETTLEMENT_DECISION),
- Some(RadrootsOrderEventType::SettlementDecision)
- );
+ assert_eq!(RadrootsOrderEventType::from_kind(3433), None);
+ assert_eq!(RadrootsOrderEventType::from_kind(3434), None);
+ assert_eq!(RadrootsOrderEventType::from_kind(3435), None);
+ assert_eq!(RadrootsOrderEventType::from_kind(3436), None);
assert_eq!(RadrootsOrderEventType::from_kind(3431), None);
assert_eq!(
RadrootsOrderEventType::OrderRequested.kind(),
@@ -1401,26 +1082,10 @@ mod tests {
KIND_ORDER_REVISION_DECISION
);
assert_eq!(
- RadrootsOrderEventType::FulfillmentUpdated.kind(),
- KIND_ORDER_FULFILLMENT_UPDATE
- );
- assert_eq!(
RadrootsOrderEventType::OrderCancelled.kind(),
KIND_ORDER_CANCELLATION
);
assert_eq!(
- RadrootsOrderEventType::BuyerReceipt.kind(),
- KIND_ORDER_RECEIPT
- );
- assert_eq!(
- RadrootsOrderEventType::PaymentRecorded.kind(),
- KIND_ORDER_PAYMENT_RECORD
- );
- assert_eq!(
- RadrootsOrderEventType::SettlementDecision.kind(),
- KIND_ORDER_SETTLEMENT_DECISION
- );
- assert_eq!(
RadrootsOrderEventType::OrderRequested.name(),
"TradeOrderRequested"
);
@@ -1437,36 +1102,15 @@ mod tests {
"TradeOrderRevisionDecision"
);
assert_eq!(
- RadrootsOrderEventType::FulfillmentUpdated.name(),
- "TradeFulfillmentUpdated"
- );
- assert_eq!(
RadrootsOrderEventType::OrderCancelled.name(),
"TradeOrderCancelled"
);
- assert_eq!(
- RadrootsOrderEventType::BuyerReceipt.name(),
- "TradeBuyerReceipt"
- );
- assert_eq!(
- RadrootsOrderEventType::PaymentRecorded.name(),
- "TradePaymentRecorded"
- );
- assert_eq!(
- RadrootsOrderEventType::SettlementDecision.name(),
- "TradeSettlementDecision"
- );
assert!(RadrootsOrderEventType::OrderRequested.requires_listing_snapshot());
assert!(RadrootsOrderEventType::OrderDecision.requires_order_chain());
assert!(RadrootsOrderEventType::OrderRevisionProposed.requires_order_chain());
assert!(RadrootsOrderEventType::OrderRevisionDecision.requires_order_chain());
- assert!(RadrootsOrderEventType::FulfillmentUpdated.requires_order_chain());
assert!(RadrootsOrderEventType::OrderCancelled.requires_order_chain());
- assert!(RadrootsOrderEventType::BuyerReceipt.requires_order_chain());
- assert!(RadrootsOrderEventType::PaymentRecorded.requires_order_chain());
- assert!(RadrootsOrderEventType::SettlementDecision.requires_order_chain());
assert!(!RadrootsOrderEventType::OrderRequested.requires_order_chain());
- assert!(!RadrootsOrderEventType::PaymentRecorded.requires_listing_snapshot());
let request_name = serde_json::to_value(RadrootsOrderEventType::OrderRequested).unwrap();
let decision_name = serde_json::to_value(RadrootsOrderEventType::OrderDecision).unwrap();
@@ -1474,14 +1118,8 @@ mod tests {
serde_json::to_value(RadrootsOrderEventType::OrderRevisionProposed).unwrap();
let revision_decision_name =
serde_json::to_value(RadrootsOrderEventType::OrderRevisionDecision).unwrap();
- let fulfillment_name =
- serde_json::to_value(RadrootsOrderEventType::FulfillmentUpdated).unwrap();
let cancellation_name =
serde_json::to_value(RadrootsOrderEventType::OrderCancelled).unwrap();
- let receipt_name = serde_json::to_value(RadrootsOrderEventType::BuyerReceipt).unwrap();
- let payment_name = serde_json::to_value(RadrootsOrderEventType::PaymentRecorded).unwrap();
- let settlement_name =
- serde_json::to_value(RadrootsOrderEventType::SettlementDecision).unwrap();
assert_eq!(request_name, serde_json::json!("TradeOrderRequested"));
assert_eq!(decision_name, serde_json::json!("TradeOrderDecision"));
assert_eq!(
@@ -1492,17 +1130,7 @@ mod tests {
revision_decision_name,
serde_json::json!("TradeOrderRevisionDecision")
);
- assert_eq!(
- fulfillment_name,
- serde_json::json!("TradeFulfillmentUpdated")
- );
assert_eq!(cancellation_name, serde_json::json!("TradeOrderCancelled"));
- assert_eq!(receipt_name, serde_json::json!("TradeBuyerReceipt"));
- assert_eq!(payment_name, serde_json::json!("TradePaymentRecorded"));
- assert_eq!(
- settlement_name,
- serde_json::json!("TradeSettlementDecision")
- );
}
#[test]
@@ -1554,18 +1182,6 @@ mod tests {
let mut revision = serde_json::to_value(sample_order_revision_proposal()).unwrap();
revision["root_event_id"] = serde_json::json!("not-an-event-id");
assert!(serde_json::from_value::<RadrootsOrderRevisionProposal>(revision).is_err());
-
- let mut payment = serde_json::to_value(sample_payment_recorded()).unwrap();
- payment["agreement_event_id"] = serde_json::json!("not-an-event-id");
- assert!(serde_json::from_value::<RadrootsOrderPaymentRecord>(payment).is_err());
-
- let mut settlement = serde_json::to_value(sample_settlement_decision(
- RadrootsOrderSettlementOutcome::Accepted,
- None,
- ))
- .unwrap();
- settlement["payment_event_id"] = serde_json::json!("not-an-event-id");
- assert!(serde_json::from_value::<RadrootsOrderSettlementDecision>(settlement).is_err());
}
#[test]
@@ -1985,20 +1601,6 @@ mod tests {
}
#[test]
- fn order_fulfillment_update_validation_rejects_derived_state() {
- assert_eq!(sample_order_fulfillment_update().validate(), Ok(()));
-
- let derived = RadrootsOrderFulfillmentUpdate {
- status: RadrootsOrderFulfillmentState::AcceptedNotFulfilled,
- ..sample_order_fulfillment_update()
- };
- assert_eq!(
- derived.validate().unwrap_err(),
- RadrootsOrderPayloadError::InvalidFulfillmentStatus
- );
- }
-
- #[test]
fn order_cancellation_validation_requires_buyer_bindings_and_reason() {
assert_eq!(sample_order_cancellation().validate(), Ok(()));
@@ -2013,144 +1615,6 @@ mod tests {
}
#[test]
- fn order_buyer_receipt_validation_requires_consistent_received_and_issue() {
- assert_eq!(sample_order_buyer_receipt(true).validate(), Ok(()));
- assert_eq!(sample_order_buyer_receipt(false).validate(), Ok(()));
-
- let received_with_issue = RadrootsOrderReceipt {
- issue: Some("damaged".into()),
- ..sample_order_buyer_receipt(true)
- };
- assert_eq!(
- received_with_issue.validate().unwrap_err(),
- RadrootsOrderPayloadError::UnexpectedReceiptIssue
- );
-
- let not_received_without_issue = RadrootsOrderReceipt {
- issue: None,
- ..sample_order_buyer_receipt(false)
- };
- assert_eq!(
- not_received_without_issue.validate().unwrap_err(),
- RadrootsOrderPayloadError::MissingReceiptIssue
- );
-
- let not_received_blank_issue = RadrootsOrderReceipt {
- issue: Some(" ".into()),
- ..sample_order_buyer_receipt(false)
- };
- assert_eq!(
- not_received_blank_issue.validate().unwrap_err(),
- RadrootsOrderPayloadError::EmptyField("issue")
- );
- }
-
- #[test]
- fn order_payment_and_settlement_validation_covers_amount_and_reason_paths() {
- assert_eq!(sample_payment_recorded().validate(), Ok(()));
-
- let unreferenced_payment = RadrootsOrderPaymentRecord {
- reference: None,
- ..sample_payment_recorded()
- };
- assert_eq!(unreferenced_payment.validate(), Ok(()));
-
- let invalid_quote_version = RadrootsOrderPaymentRecord {
- quote_version: 0,
- ..sample_payment_recorded()
- };
- assert_eq!(
- invalid_quote_version.validate().unwrap_err(),
- RadrootsOrderPayloadError::InvalidQuoteVersion
- );
-
- let invalid_amount = RadrootsOrderPaymentRecord {
- amount: decimal("0"),
- ..sample_payment_recorded()
- };
- assert_eq!(
- invalid_amount.validate().unwrap_err(),
- RadrootsOrderPayloadError::InvalidPaymentAmount
- );
-
- let negative_amount = RadrootsOrderPaymentRecord {
- amount: decimal("-1"),
- ..sample_payment_recorded()
- };
- assert_eq!(
- negative_amount.validate().unwrap_err(),
- RadrootsOrderPayloadError::InvalidPaymentAmount
- );
-
- let blank_reference = RadrootsOrderPaymentRecord {
- reference: Some(" ".into()),
- ..sample_payment_recorded()
- };
- assert_eq!(
- blank_reference.validate().unwrap_err(),
- RadrootsOrderPayloadError::EmptyField("reference")
- );
-
- assert_eq!(
- sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None).validate(),
- Ok(())
- );
- assert_eq!(
- sample_settlement_decision(RadrootsOrderSettlementOutcome::Rejected, Some("damaged"))
- .validate(),
- Ok(())
- );
-
- let accepted_with_reason =
- sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, Some("extra"));
- assert_eq!(
- accepted_with_reason.validate().unwrap_err(),
- RadrootsOrderPayloadError::UnexpectedSettlementReason
- );
-
- let rejected_without_reason =
- sample_settlement_decision(RadrootsOrderSettlementOutcome::Rejected, None);
- assert_eq!(
- rejected_without_reason.validate().unwrap_err(),
- RadrootsOrderPayloadError::MissingSettlementReason
- );
-
- let rejected_blank_reason =
- sample_settlement_decision(RadrootsOrderSettlementOutcome::Rejected, Some(" "));
- assert_eq!(
- rejected_blank_reason.validate().unwrap_err(),
- RadrootsOrderPayloadError::EmptyField("reason")
- );
-
- let invalid_quote_version = RadrootsOrderSettlementDecision {
- quote_version: 0,
- ..sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None)
- };
- assert_eq!(
- invalid_quote_version.validate().unwrap_err(),
- RadrootsOrderPayloadError::InvalidQuoteVersion
- );
-
- let zero_amount = RadrootsOrderSettlementDecision {
- amount: decimal("0"),
- ..sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None)
- };
- assert_eq!(
- zero_amount.validate().unwrap_err(),
- RadrootsOrderPayloadError::InvalidPaymentAmount
- );
-
- let invalid_amount = RadrootsOrderSettlementDecision {
- amount: decimal("-1"),
- ..sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None)
- };
- assert_eq!(
- invalid_amount.validate().unwrap_err(),
- RadrootsOrderPayloadError::InvalidPaymentAmount
- );
- }
-
- #[test]
fn order_envelope_serializes_canonical_type_name() {
let envelope = RadrootsOrderEnvelope::new(
RadrootsOrderEventType::OrderRequested,
@@ -2440,30 +1904,6 @@ mod tests {
RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { index: 1 },
"inventory_commitments[1].bin_count must be greater than zero",
),
- (
- RadrootsOrderPayloadError::InvalidFulfillmentStatus,
- "fulfillment status is not publishable",
- ),
- (
- RadrootsOrderPayloadError::MissingReceiptIssue,
- "receipt issue is required when received is false",
- ),
- (
- RadrootsOrderPayloadError::UnexpectedReceiptIssue,
- "receipt issue must be absent when received is true",
- ),
- (
- RadrootsOrderPayloadError::InvalidPaymentAmount,
- "payment amount must be greater than zero",
- ),
- (
- RadrootsOrderPayloadError::MissingSettlementReason,
- "settlement reason is required when decision is rejected",
- ),
- (
- RadrootsOrderPayloadError::UnexpectedSettlementReason,
- "settlement reason must be absent when decision is accepted",
- ),
];
for (error, expected) in cases {
diff --git a/crates/events_codec/src/order/decode.rs b/crates/events_codec/src/order/decode.rs
@@ -8,10 +8,8 @@ use radroots_events::{
kinds::is_order_event_kind,
order::{
RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderEnvelope,
- RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderFulfillmentUpdate,
- RadrootsOrderPayloadError, RadrootsOrderPaymentRecord, RadrootsOrderReceipt,
+ RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderPayloadError,
RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal,
- RadrootsOrderSettlementDecision,
},
tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT},
};
@@ -263,33 +261,6 @@ pub fn order_revision_decision_from_event(
}
#[cfg(feature = "serde_json")]
-pub fn order_fulfillment_update_from_event(
- event: &RadrootsNostrEvent,
-) -> Result<RadrootsOrderEnvelope<RadrootsOrderFulfillmentUpdate>, RadrootsOrderEnvelopeParseError>
-{
- let envelope = order_envelope_from_event::<RadrootsOrderFulfillmentUpdate>(event)?;
- if envelope.message_type != RadrootsOrderEventType::FulfillmentUpdated {
- return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch {
- event_kind: event.kind,
- message_type: envelope.message_type,
- });
- }
- envelope
- .payload
- .validate()
- .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?;
- validate_order_binding(
- event,
- &envelope,
- &envelope.payload.order_id,
- &envelope.payload.listing_addr,
- &envelope.payload.seller_pubkey,
- &envelope.payload.buyer_pubkey,
- )?;
- Ok(envelope)
-}
-
-#[cfg(feature = "serde_json")]
pub fn order_cancellation_from_event(
event: &RadrootsNostrEvent,
) -> Result<RadrootsOrderEnvelope<RadrootsOrderCancellation>, RadrootsOrderEnvelopeParseError> {
@@ -316,107 +287,6 @@ pub fn order_cancellation_from_event(
}
#[cfg(feature = "serde_json")]
-pub fn order_receipt_from_event(
- event: &RadrootsNostrEvent,
-) -> Result<RadrootsOrderEnvelope<RadrootsOrderReceipt>, RadrootsOrderEnvelopeParseError> {
- let envelope = order_envelope_from_event::<RadrootsOrderReceipt>(event)?;
- if envelope.message_type != RadrootsOrderEventType::BuyerReceipt {
- return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch {
- event_kind: event.kind,
- message_type: envelope.message_type,
- });
- }
- envelope
- .payload
- .validate()
- .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?;
- validate_order_binding(
- event,
- &envelope,
- &envelope.payload.order_id,
- &envelope.payload.listing_addr,
- &envelope.payload.buyer_pubkey,
- &envelope.payload.seller_pubkey,
- )?;
- Ok(envelope)
-}
-
-#[cfg(feature = "serde_json")]
-pub fn order_payment_record_from_event(
- event: &RadrootsNostrEvent,
-) -> Result<RadrootsOrderEnvelope<RadrootsOrderPaymentRecord>, RadrootsOrderEnvelopeParseError> {
- let envelope = order_envelope_from_event::<RadrootsOrderPaymentRecord>(event)?;
- if envelope.message_type != RadrootsOrderEventType::PaymentRecorded {
- return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch {
- event_kind: event.kind,
- message_type: envelope.message_type,
- });
- }
- envelope
- .payload
- .validate()
- .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?;
- validate_order_binding(
- event,
- &envelope,
- &envelope.payload.order_id,
- &envelope.payload.listing_addr,
- &envelope.payload.buyer_pubkey,
- &envelope.payload.seller_pubkey,
- )?;
- let context = order_event_context_from_tags(envelope.message_type, &event.tags)?;
- if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) {
- return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch(
- "root_event_id",
- ));
- }
- if context.prev_event_id.as_deref() != Some(envelope.payload.previous_event_id.as_str()) {
- return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch(
- "previous_event_id",
- ));
- }
- Ok(envelope)
-}
-
-#[cfg(feature = "serde_json")]
-pub fn order_settlement_decision_from_event(
- event: &RadrootsNostrEvent,
-) -> Result<RadrootsOrderEnvelope<RadrootsOrderSettlementDecision>, RadrootsOrderEnvelopeParseError>
-{
- let envelope = order_envelope_from_event::<RadrootsOrderSettlementDecision>(event)?;
- if envelope.message_type != RadrootsOrderEventType::SettlementDecision {
- return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch {
- event_kind: event.kind,
- message_type: envelope.message_type,
- });
- }
- envelope
- .payload
- .validate()
- .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?;
- validate_order_binding(
- event,
- &envelope,
- &envelope.payload.order_id,
- &envelope.payload.listing_addr,
- &envelope.payload.seller_pubkey,
- &envelope.payload.buyer_pubkey,
- )?;
- let context = order_event_context_from_tags(envelope.message_type, &event.tags)?;
- if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) {
- return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch(
- "root_event_id",
- ));
- }
- if context.prev_event_id.as_deref() != Some(envelope.payload.previous_event_id.as_str()) {
- return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch(
- "previous_event_id",
- ));
- }
- Ok(envelope)
-}
-
-#[cfg(feature = "serde_json")]
pub fn order_event_context_from_tags(
message_type: RadrootsOrderEventType,
tags: &[Vec<String>],
@@ -539,16 +409,12 @@ fn validate_order_binding<T>(
mod tests {
use super::{
RadrootsOrderEnvelopeParseError, order_cancellation_from_event, order_decision_from_event,
- order_envelope_from_event, order_fulfillment_update_from_event,
- order_payment_record_from_event, order_receipt_from_event, order_request_from_event,
- order_revision_decision_from_event, order_revision_proposal_from_event,
- order_settlement_decision_from_event,
+ order_envelope_from_event, order_request_from_event, order_revision_decision_from_event,
+ order_revision_proposal_from_event,
};
use crate::order::encode::{
- order_cancellation_event_build, order_decision_event_build,
- order_fulfillment_update_event_build, order_payment_record_event_build,
- order_receipt_event_build, order_request_event_build, order_revision_decision_event_build,
- order_revision_proposal_event_build, order_settlement_decision_event_build,
+ order_cancellation_event_build, order_decision_event_build, order_request_event_build,
+ order_revision_decision_event_build, order_revision_proposal_event_build,
};
use crate::order::tags::TAG_LISTING_EVENT;
use radroots_core::{
@@ -557,26 +423,20 @@ mod tests {
use radroots_events::{
RadrootsNostrEvent, RadrootsNostrEventPtr,
ids::{
- RadrootsEconomicsDigest, RadrootsEventId, RadrootsInventoryBinId,
- RadrootsListingAddress, RadrootsOrderId, RadrootsOrderQuoteId, RadrootsOrderRevisionId,
- RadrootsPublicKey,
+ RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
+ RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey,
},
kinds::{
- KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_FULFILLMENT_UPDATE,
- KIND_ORDER_PAYMENT_RECORD, KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST,
+ KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST,
KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL,
- KIND_ORDER_SETTLEMENT_DECISION,
},
order::{
RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics,
- RadrootsOrderEnvelope, RadrootsOrderEventType, RadrootsOrderFulfillmentState,
- RadrootsOrderFulfillmentUpdate, RadrootsOrderInventoryCommitment, RadrootsOrderItem,
- RadrootsOrderPayloadError, RadrootsOrderPaymentMethod, RadrootsOrderPaymentRecord,
- RadrootsOrderPricingBasis, RadrootsOrderReceipt, RadrootsOrderRequest,
- RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome,
- RadrootsOrderRevisionProposal, RadrootsOrderSettlementDecision,
- RadrootsOrderSettlementOutcome,
+ RadrootsOrderEnvelope, RadrootsOrderEventType, RadrootsOrderInventoryCommitment,
+ RadrootsOrderItem, RadrootsOrderPayloadError, RadrootsOrderPricingBasis,
+ RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome,
+ RadrootsOrderRevisionProposal,
},
tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT},
};
@@ -630,10 +490,6 @@ mod tests {
raw.parse().unwrap()
}
- fn digest(raw: &str) -> RadrootsEconomicsDigest {
- raw.parse().unwrap()
- }
-
fn event_id(character: char) -> RadrootsEventId {
core::iter::repeat_n(character, 64)
.collect::<String>()
@@ -747,16 +603,6 @@ mod tests {
}
}
- fn order_fulfillment_update() -> RadrootsOrderFulfillmentUpdate {
- RadrootsOrderFulfillmentUpdate {
- order_id: order_id("order-1"),
- listing_addr: listing_addr(),
- buyer_pubkey: buyer_pubkey(),
- seller_pubkey: seller_pubkey(),
- status: RadrootsOrderFulfillmentState::ReadyForPickup,
- }
- }
-
fn order_cancelled() -> RadrootsOrderCancellation {
RadrootsOrderCancellation {
order_id: order_id("order-1"),
@@ -767,61 +613,6 @@ mod tests {
}
}
- fn order_buyer_receipt(received: bool) -> RadrootsOrderReceipt {
- RadrootsOrderReceipt {
- order_id: order_id("order-1"),
- listing_addr: listing_addr(),
- buyer_pubkey: buyer_pubkey(),
- seller_pubkey: seller_pubkey(),
- received,
- issue: (!received).then(|| "damaged items".into()),
- received_at: 1_777_665_600,
- }
- }
-
- fn order_payment_recorded() -> RadrootsOrderPaymentRecord {
- RadrootsOrderPaymentRecord {
- order_id: order_id("order-1"),
- listing_addr: listing_addr(),
- buyer_pubkey: buyer_pubkey(),
- seller_pubkey: seller_pubkey(),
- root_event_id: event_id('1'),
- previous_event_id: event_id('4'),
- agreement_event_id: event_id('4'),
- quote_id: quote_id("quote-1"),
- quote_version: 1,
- economics_digest: digest("digest-1"),
- amount: decimal("15"),
- currency: RadrootsCoreCurrency::USD,
- method: RadrootsOrderPaymentMethod::Cash,
- reference: Some("cash drawer".into()),
- paid_at: Some(1_777_665_600),
- }
- }
-
- fn order_settlement_decision(
- decision: RadrootsOrderSettlementOutcome,
- ) -> RadrootsOrderSettlementDecision {
- RadrootsOrderSettlementDecision {
- order_id: order_id("order-1"),
- listing_addr: listing_addr(),
- seller_pubkey: seller_pubkey(),
- buyer_pubkey: buyer_pubkey(),
- root_event_id: event_id('1'),
- previous_event_id: event_id('5'),
- agreement_event_id: event_id('4'),
- payment_event_id: event_id('5'),
- quote_id: quote_id("quote-1"),
- quote_version: 1,
- economics_digest: digest("digest-1"),
- amount: decimal("15"),
- currency: RadrootsCoreCurrency::USD,
- decision,
- reason: (decision == RadrootsOrderSettlementOutcome::Rejected)
- .then(|| "reference mismatch".into()),
- }
- }
-
fn listing_event_ptr() -> RadrootsNostrEventPtr {
RadrootsNostrEventPtr {
id: event_id_wire('a'),
@@ -977,41 +768,6 @@ mod tests {
}
#[test]
- fn order_fulfillment_update_builder_emits_canonical_chain_shape() {
- let payload = order_fulfillment_update();
- let root_event_id = event_id('1');
- let prev_event_id = event_id('9');
- let built =
- order_fulfillment_update_event_build(&root_event_id, &prev_event_id, &payload).unwrap();
- let envelope: RadrootsOrderEnvelope<RadrootsOrderFulfillmentUpdate> =
- serde_json::from_str(&built.content).unwrap();
-
- assert_eq!(built.kind, KIND_ORDER_FULFILLMENT_UPDATE);
- assert_eq!(
- envelope.message_type,
- RadrootsOrderEventType::FulfillmentUpdated
- );
- assert_eq!(envelope.payload.status, payload.status);
- assert_eq!(built.tags[0], vec!["p".to_string(), buyer_pubkey_wire()]);
- assert_eq!(
- built.tags[2],
- vec![TAG_D.to_string(), "order-1".to_string()]
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), event_id_wire('1')])
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_PREV.to_string(), event_id_wire('9')])
- );
- }
-
- #[test]
fn order_cancellation_builder_emits_canonical_buyer_chain_shape() {
let payload = order_cancelled();
let root_event_id = event_id('1');
@@ -1047,117 +803,6 @@ mod tests {
}
#[test]
- fn order_buyer_receipt_builder_emits_canonical_buyer_chain_shape() {
- let payload = order_buyer_receipt(false);
- let root_event_id = event_id('1');
- let prev_event_id = event_id('9');
- let built = order_receipt_event_build(&root_event_id, &prev_event_id, &payload).unwrap();
- let envelope: RadrootsOrderEnvelope<RadrootsOrderReceipt> =
- serde_json::from_str(&built.content).unwrap();
-
- assert_eq!(built.kind, KIND_ORDER_RECEIPT);
- assert_eq!(envelope.message_type, RadrootsOrderEventType::BuyerReceipt);
- assert_eq!(envelope.payload.received, false);
- assert_eq!(envelope.payload.issue.as_deref(), Some("damaged items"));
- assert_eq!(built.tags[0], vec!["p".to_string(), seller_pubkey_wire()]);
- assert_eq!(
- built.tags[2],
- vec![TAG_D.to_string(), "order-1".to_string()]
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), event_id_wire('1')])
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_PREV.to_string(), event_id_wire('9')])
- );
- }
-
- #[test]
- fn order_payment_recorded_builder_emits_canonical_buyer_chain_shape() {
- let payload = order_payment_recorded();
- let built = order_payment_record_event_build(
- &payload.root_event_id,
- &payload.previous_event_id,
- &payload,
- )
- .unwrap();
- let envelope: RadrootsOrderEnvelope<RadrootsOrderPaymentRecord> =
- serde_json::from_str(&built.content).unwrap();
-
- assert_eq!(built.kind, KIND_ORDER_PAYMENT_RECORD);
- assert_eq!(
- envelope.message_type,
- RadrootsOrderEventType::PaymentRecorded
- );
- assert_eq!(envelope.payload.amount, decimal("15"));
- assert_eq!(envelope.payload.method, RadrootsOrderPaymentMethod::Cash);
- assert_eq!(built.tags[0], vec!["p".to_string(), seller_pubkey_wire()]);
- assert_eq!(
- built.tags[2],
- vec![TAG_D.to_string(), "order-1".to_string()]
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), event_id_wire('1')])
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_PREV.to_string(), event_id_wire('4')])
- );
- }
-
- #[test]
- fn order_settlement_decision_builder_emits_canonical_seller_chain_shape() {
- let payload = order_settlement_decision(RadrootsOrderSettlementOutcome::Accepted);
- let built = order_settlement_decision_event_build(
- &payload.root_event_id,
- &payload.previous_event_id,
- &payload,
- )
- .unwrap();
- let envelope: RadrootsOrderEnvelope<RadrootsOrderSettlementDecision> =
- serde_json::from_str(&built.content).unwrap();
-
- assert_eq!(built.kind, KIND_ORDER_SETTLEMENT_DECISION);
- assert_eq!(
- envelope.message_type,
- RadrootsOrderEventType::SettlementDecision
- );
- assert_eq!(
- envelope.payload.decision,
- RadrootsOrderSettlementOutcome::Accepted
- );
- assert_eq!(envelope.payload.reason, None);
- assert_eq!(built.tags[0], vec!["p".to_string(), buyer_pubkey_wire()]);
- assert_eq!(
- built.tags[2],
- vec![TAG_D.to_string(), "order-1".to_string()]
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), event_id_wire('1')])
- );
- assert!(
- built
- .tags
- .iter()
- .any(|tag| tag == &vec![TAG_E_PREV.to_string(), event_id_wire('5')])
- );
- }
-
- #[test]
fn order_request_parse_roundtrips_and_validates_tags() {
let payload = order_request();
let built = order_request_event_build(&listing_event_ptr(), &payload).unwrap();
@@ -1232,31 +877,6 @@ mod tests {
}
#[test]
- fn order_fulfillment_update_parse_roundtrips_and_validates_chain_tags() {
- let payload = order_fulfillment_update();
- let root_event_id = event_id('1');
- let prev_event_id = event_id('9');
- let built =
- order_fulfillment_update_event_build(&root_event_id, &prev_event_id, &payload).unwrap();
- let event = RadrootsNostrEvent {
- id: event_id_wire('e'),
- author: seller_pubkey_wire(),
- created_at: 1,
- kind: built.kind,
- tags: built.tags,
- content: built.content,
- sig: "sig".into(),
- };
- let envelope = order_fulfillment_update_from_event(&event).unwrap();
-
- assert_eq!(envelope.payload, payload);
- assert_eq!(
- envelope.message_type,
- RadrootsOrderEventType::FulfillmentUpdated
- );
- }
-
- #[test]
fn order_cancellation_parse_roundtrips_and_validates_buyer_actor() {
let payload = order_cancelled();
let root_event_id = event_id('1');
@@ -1282,81 +902,6 @@ mod tests {
}
#[test]
- fn order_buyer_receipt_parse_roundtrips_and_validates_buyer_actor() {
- let payload = order_buyer_receipt(true);
- let root_event_id = event_id('1');
- let prev_event_id = event_id('9');
- let built = order_receipt_event_build(&root_event_id, &prev_event_id, &payload).unwrap();
- let event = RadrootsNostrEvent {
- id: event_id_wire('e'),
- author: buyer_pubkey_wire(),
- created_at: 1,
- kind: built.kind,
- tags: built.tags,
- content: built.content,
- sig: "sig".into(),
- };
- let envelope = order_receipt_from_event(&event).unwrap();
-
- assert_eq!(envelope.payload, payload);
- assert_eq!(envelope.message_type, RadrootsOrderEventType::BuyerReceipt);
- }
-
- #[test]
- fn order_payment_recorded_parse_roundtrips_and_validates_buyer_actor() {
- let payload = order_payment_recorded();
- let built = order_payment_record_event_build(
- &payload.root_event_id,
- &payload.previous_event_id,
- &payload,
- )
- .unwrap();
- let event = RadrootsNostrEvent {
- id: event_id_wire('f'),
- author: buyer_pubkey_wire(),
- created_at: 1,
- kind: built.kind,
- tags: built.tags,
- content: built.content,
- sig: "sig".into(),
- };
- let envelope = order_payment_record_from_event(&event).unwrap();
-
- assert_eq!(envelope.payload, payload);
- assert_eq!(
- envelope.message_type,
- RadrootsOrderEventType::PaymentRecorded
- );
- }
-
- #[test]
- fn order_settlement_decision_parse_roundtrips_and_validates_seller_actor() {
- let payload = order_settlement_decision(RadrootsOrderSettlementOutcome::Rejected);
- let built = order_settlement_decision_event_build(
- &payload.root_event_id,
- &payload.previous_event_id,
- &payload,
- )
- .unwrap();
- let event = RadrootsNostrEvent {
- id: event_id_wire('e'),
- author: seller_pubkey_wire(),
- created_at: 1,
- kind: built.kind,
- tags: built.tags,
- content: built.content,
- sig: "sig".into(),
- };
- let envelope = order_settlement_decision_from_event(&event).unwrap();
-
- assert_eq!(envelope.payload, payload);
- assert_eq!(
- envelope.message_type,
- RadrootsOrderEventType::SettlementDecision
- );
- }
-
- #[test]
fn order_revision_proposal_parse_validates_actor_counterparty_and_chain_payload() {
let payload = order_revision_proposal();
let built = order_revision_proposal_event_build(
@@ -1511,7 +1056,7 @@ mod tests {
}
#[test]
- fn order_buyer_lifecycle_parse_rejects_wrong_actor_or_counterparty() {
+ fn order_cancellation_parse_rejects_wrong_actor() {
let cancellation = order_cancelled();
let root_event_id = event_id('1');
let prev_event_id = event_id('9');
@@ -1528,25 +1073,6 @@ mod tests {
};
let err = order_cancellation_from_event(&cancellation_event).unwrap_err();
assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch);
-
- let receipt = order_buyer_receipt(true);
- let receipt_parts =
- order_receipt_event_build(&root_event_id, &prev_event_id, &receipt).unwrap();
- let mut receipt_event = RadrootsNostrEvent {
- id: event_id_wire('e'),
- author: buyer_pubkey_wire(),
- created_at: 1,
- kind: receipt_parts.kind,
- tags: receipt_parts.tags,
- content: receipt_parts.content,
- sig: "sig".into(),
- };
- receipt_event.tags[0] = vec!["p".into(), pubkey('c').into_string()];
- let err = order_receipt_from_event(&receipt_event).unwrap_err();
- assert_eq!(
- err,
- RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch
- );
}
#[test]
diff --git a/crates/events_codec/src/order/encode.rs b/crates/events_codec/src/order/encode.rs
@@ -7,10 +7,8 @@ use radroots_events::{
ids::RadrootsEventId,
order::{
RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderEnvelope,
- RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderFulfillmentUpdate,
- RadrootsOrderPayloadError, RadrootsOrderPaymentRecord, RadrootsOrderReceipt,
+ RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderPayloadError,
RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal,
- RadrootsOrderSettlementDecision,
},
};
@@ -73,24 +71,6 @@ fn map_order_payload_error(error: RadrootsOrderPayloadError) -> EventEncodeError
RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { .. } => {
EventEncodeError::InvalidField("inventory_commitments.bin_count")
}
- RadrootsOrderPayloadError::InvalidFulfillmentStatus => {
- EventEncodeError::InvalidField("fulfillment.status")
- }
- RadrootsOrderPayloadError::MissingReceiptIssue => {
- EventEncodeError::EmptyRequiredField("receipt.issue")
- }
- RadrootsOrderPayloadError::UnexpectedReceiptIssue => {
- EventEncodeError::InvalidField("receipt.issue")
- }
- RadrootsOrderPayloadError::InvalidPaymentAmount => {
- EventEncodeError::InvalidField("payment.amount")
- }
- RadrootsOrderPayloadError::MissingSettlementReason => {
- EventEncodeError::EmptyRequiredField("settlement.reason")
- }
- RadrootsOrderPayloadError::UnexpectedSettlementReason => {
- EventEncodeError::InvalidField("settlement.reason")
- }
}
}
@@ -233,25 +213,6 @@ pub fn order_revision_decision_event_build(
}
#[cfg(feature = "serde_json")]
-pub fn order_fulfillment_update_event_build(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderFulfillmentUpdate,
-) -> Result<WireEventParts, EventEncodeError> {
- payload.validate().map_err(map_order_payload_error)?;
- order_envelope_event_build(OrderEnvelopeEventBuildParts {
- recipient_pubkey: &payload.buyer_pubkey,
- message_type: RadrootsOrderEventType::FulfillmentUpdated,
- listing_addr: &payload.listing_addr,
- order_id: &payload.order_id,
- listing_event: None,
- root_event_id: Some(root_event_id),
- prev_event_id: Some(prev_event_id),
- payload,
- })
-}
-
-#[cfg(feature = "serde_json")]
pub fn order_cancellation_event_build(
root_event_id: &RadrootsEventId,
prev_event_id: &RadrootsEventId,
@@ -269,72 +230,3 @@ pub fn order_cancellation_event_build(
payload,
})
}
-
-#[cfg(feature = "serde_json")]
-pub fn order_receipt_event_build(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderReceipt,
-) -> Result<WireEventParts, EventEncodeError> {
- payload.validate().map_err(map_order_payload_error)?;
- order_envelope_event_build(OrderEnvelopeEventBuildParts {
- recipient_pubkey: &payload.seller_pubkey,
- message_type: RadrootsOrderEventType::BuyerReceipt,
- listing_addr: &payload.listing_addr,
- order_id: &payload.order_id,
- listing_event: None,
- root_event_id: Some(root_event_id),
- prev_event_id: Some(prev_event_id),
- payload,
- })
-}
-
-#[cfg(feature = "serde_json")]
-pub fn order_payment_record_event_build(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderPaymentRecord,
-) -> Result<WireEventParts, EventEncodeError> {
- payload.validate().map_err(map_order_payload_error)?;
- if payload.root_event_id.as_str() != root_event_id.as_str() {
- return Err(EventEncodeError::InvalidField("root_event_id"));
- }
- if payload.previous_event_id.as_str() != prev_event_id.as_str() {
- return Err(EventEncodeError::InvalidField("previous_event_id"));
- }
- order_envelope_event_build(OrderEnvelopeEventBuildParts {
- recipient_pubkey: &payload.seller_pubkey,
- message_type: RadrootsOrderEventType::PaymentRecorded,
- listing_addr: &payload.listing_addr,
- order_id: &payload.order_id,
- listing_event: None,
- root_event_id: Some(root_event_id),
- prev_event_id: Some(prev_event_id),
- payload,
- })
-}
-
-#[cfg(feature = "serde_json")]
-pub fn order_settlement_decision_event_build(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderSettlementDecision,
-) -> Result<WireEventParts, EventEncodeError> {
- payload.validate().map_err(map_order_payload_error)?;
- if payload.root_event_id.as_str() != root_event_id.as_str() {
- return Err(EventEncodeError::InvalidField("root_event_id"));
- }
- if payload.previous_event_id.as_str() != prev_event_id.as_str() {
- return Err(EventEncodeError::InvalidField("previous_event_id"));
- }
- order_envelope_event_build(OrderEnvelopeEventBuildParts {
- recipient_pubkey: &payload.buyer_pubkey,
- message_type: RadrootsOrderEventType::SettlementDecision,
- listing_addr: &payload.listing_addr,
- order_id: &payload.order_id,
- listing_event: None,
- root_event_id: Some(root_event_id),
- prev_event_id: Some(prev_event_id),
- payload,
- })
-}
diff --git a/crates/events_codec/src/order/mod.rs b/crates/events_codec/src/order/mod.rs
@@ -6,16 +6,13 @@ pub mod tags;
pub use decode::{
RadrootsOrderEnvelopeParseError, RadrootsOrderEventContext, order_cancellation_from_event,
order_decision_from_event, order_envelope_from_event, order_event_context_from_tags,
- order_fulfillment_update_from_event, order_payment_record_from_event, order_receipt_from_event,
order_request_from_event, order_revision_decision_from_event,
- order_revision_proposal_from_event, order_settlement_decision_from_event,
+ order_revision_proposal_from_event,
};
#[cfg(feature = "serde_json")]
pub use encode::{
- order_cancellation_event_build, order_decision_event_build,
- order_fulfillment_update_event_build, order_payment_record_event_build,
- order_receipt_event_build, order_request_event_build, order_revision_decision_event_build,
- order_revision_proposal_event_build, order_settlement_decision_event_build,
+ order_cancellation_event_build, order_decision_event_build, order_request_event_build,
+ order_revision_decision_event_build, order_revision_proposal_event_build,
};
pub use tags::{
TAG_LISTING_EVENT, order_envelope_tags, parse_order_counterparty_tag,
diff --git a/crates/trade/src/order.rs b/crates/trade/src/order.rs
@@ -6,40 +6,34 @@ use alloc::{
vec::Vec,
};
-use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDecimal};
#[cfg(feature = "event_store")]
use radroots_event_store::{RadrootsEventStore, RadrootsEventStoreError, RadrootsStoredEvent};
#[cfg(feature = "serde_json")]
use radroots_events::RadrootsNostrEvent;
use radroots_events::ids::{
- RadrootsEconomicsDigest, RadrootsEventId, RadrootsIdParseError, RadrootsInventoryBinId,
- RadrootsListingAddress, RadrootsOrderId, RadrootsOrderQuoteId, RadrootsPublicKey,
+ RadrootsEventId, RadrootsIdParseError, RadrootsInventoryBinId, RadrootsListingAddress,
+ RadrootsOrderId, RadrootsPublicKey,
};
#[cfg(feature = "serde_json")]
use radroots_events::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,
};
#[cfg(feature = "serde_json")]
use radroots_events::order::RadrootsOrderEventType;
use radroots_events::order::{
RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
- RadrootsOrderEconomics, RadrootsOrderFulfillmentState, RadrootsOrderFulfillmentUpdate,
- RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPaymentMethod,
- RadrootsOrderPaymentRecord as RadrootsOrderPaymentPayload, RadrootsOrderReceipt,
+ RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem,
RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome,
- RadrootsOrderRevisionProposal, RadrootsOrderSettlementDecision, RadrootsOrderSettlementOutcome,
+ RadrootsOrderRevisionProposal,
};
#[cfg(feature = "event_store")]
use radroots_events::tags::TAG_D;
#[cfg(feature = "serde_json")]
use radroots_events_codec::order::{
RadrootsOrderEnvelopeParseError, order_cancellation_from_event, order_decision_from_event,
- order_event_context_from_tags, order_fulfillment_update_from_event,
- order_payment_record_from_event, order_receipt_from_event, order_request_from_event,
- order_revision_decision_from_event, order_revision_proposal_from_event,
- order_settlement_decision_from_event,
+ order_event_context_from_tags, order_request_from_event, order_revision_decision_from_event,
+ order_revision_proposal_from_event,
};
#[cfg(feature = "serde_json")]
use sha2::{Digest, Sha256};
@@ -71,16 +65,12 @@ pub enum RadrootsOrderCanonicalizationError {
InvalidInventoryCommitmentCount { index: usize },
}
-pub const ORDER_EVENT_CONTRACT_IDS: [&str; 9] = [
+pub const ORDER_EVENT_CONTRACT_IDS: [&str; 5] = [
"radroots.order.request.v1",
"radroots.order.decision.v1",
"radroots.order.revision_proposal.v1",
"radroots.order.revision_decision.v1",
"radroots.order.cancellation.v1",
- "radroots.order.fulfillment_update.v1",
- "radroots.order.receipt.v1",
- "radroots.order.payment_record.v1",
- "radroots.order.settlement_decision.v1",
];
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -121,16 +111,6 @@ pub struct RadrootsOrderRevisionDecisionRecord {
}
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderFulfillmentRecord {
- pub event_id: RadrootsEventId,
- pub author_pubkey: RadrootsPublicKey,
- pub counterparty_pubkey: RadrootsPublicKey,
- pub root_event_id: RadrootsEventId,
- pub prev_event_id: RadrootsEventId,
- pub payload: RadrootsOrderFulfillmentUpdate,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsOrderCancellationRecord {
pub event_id: RadrootsEventId,
pub author_pubkey: RadrootsPublicKey,
@@ -141,46 +121,12 @@ pub struct RadrootsOrderCancellationRecord {
}
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderReceiptRecord {
- pub event_id: RadrootsEventId,
- pub author_pubkey: RadrootsPublicKey,
- pub counterparty_pubkey: RadrootsPublicKey,
- pub root_event_id: RadrootsEventId,
- pub prev_event_id: RadrootsEventId,
- pub payload: RadrootsOrderReceipt,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderPaymentEventRecord {
- pub event_id: RadrootsEventId,
- pub author_pubkey: RadrootsPublicKey,
- pub counterparty_pubkey: RadrootsPublicKey,
- pub root_event_id: RadrootsEventId,
- pub prev_event_id: RadrootsEventId,
- pub payload: RadrootsOrderPaymentPayload,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderSettlementRecord {
- pub event_id: RadrootsEventId,
- pub author_pubkey: RadrootsPublicKey,
- pub counterparty_pubkey: RadrootsPublicKey,
- pub root_event_id: RadrootsEventId,
- pub prev_event_id: RadrootsEventId,
- pub payload: RadrootsOrderSettlementDecision,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RadrootsOrderEventRecord {
Request(RadrootsOrderRequestRecord),
Decision(RadrootsOrderDecisionRecord),
RevisionProposal(RadrootsOrderRevisionProposalRecord),
RevisionDecision(RadrootsOrderRevisionDecisionRecord),
- Fulfillment(RadrootsOrderFulfillmentRecord),
Cancellation(RadrootsOrderCancellationRecord),
- Receipt(RadrootsOrderReceiptRecord),
- Payment(RadrootsOrderPaymentEventRecord),
- Settlement(RadrootsOrderSettlementRecord),
}
impl RadrootsOrderEventRecord {
@@ -190,11 +136,7 @@ impl RadrootsOrderEventRecord {
Self::Decision(record) => &record.event_id,
Self::RevisionProposal(record) => &record.event_id,
Self::RevisionDecision(record) => &record.event_id,
- Self::Fulfillment(record) => &record.event_id,
Self::Cancellation(record) => &record.event_id,
- Self::Receipt(record) => &record.event_id,
- Self::Payment(record) => &record.event_id,
- Self::Settlement(record) => &record.event_id,
}
}
@@ -204,11 +146,7 @@ impl RadrootsOrderEventRecord {
Self::Decision(record) => &record.payload.order_id,
Self::RevisionProposal(record) => &record.payload.order_id,
Self::RevisionDecision(record) => &record.payload.order_id,
- Self::Fulfillment(record) => &record.payload.order_id,
Self::Cancellation(record) => &record.payload.order_id,
- Self::Receipt(record) => &record.payload.order_id,
- Self::Payment(record) => &record.payload.order_id,
- Self::Settlement(record) => &record.payload.order_id,
}
}
}
@@ -292,19 +230,6 @@ pub fn order_event_record_from_event(
},
))
}
- KIND_ORDER_FULFILLMENT_UPDATE => {
- let envelope = order_fulfillment_update_from_event(event)?;
- Ok(RadrootsOrderEventRecord::Fulfillment(
- RadrootsOrderFulfillmentRecord {
- event_id,
- author_pubkey,
- counterparty_pubkey: context.counterparty_pubkey.clone(),
- root_event_id: require_context_root_event_id(&context)?,
- prev_event_id: require_context_prev_event_id(&context)?,
- payload: envelope.payload,
- },
- ))
- }
KIND_ORDER_CANCELLATION => {
let envelope = order_cancellation_from_event(event)?;
Ok(RadrootsOrderEventRecord::Cancellation(
@@ -318,45 +243,6 @@ pub fn order_event_record_from_event(
},
))
}
- KIND_ORDER_RECEIPT => {
- let envelope = order_receipt_from_event(event)?;
- Ok(RadrootsOrderEventRecord::Receipt(
- RadrootsOrderReceiptRecord {
- event_id,
- author_pubkey,
- counterparty_pubkey: context.counterparty_pubkey.clone(),
- root_event_id: require_context_root_event_id(&context)?,
- prev_event_id: require_context_prev_event_id(&context)?,
- payload: envelope.payload,
- },
- ))
- }
- KIND_ORDER_PAYMENT_RECORD => {
- let envelope = order_payment_record_from_event(event)?;
- Ok(RadrootsOrderEventRecord::Payment(
- RadrootsOrderPaymentEventRecord {
- event_id,
- author_pubkey,
- counterparty_pubkey: context.counterparty_pubkey.clone(),
- root_event_id: require_context_root_event_id(&context)?,
- prev_event_id: require_context_prev_event_id(&context)?,
- payload: envelope.payload,
- },
- ))
- }
- KIND_ORDER_SETTLEMENT_DECISION => {
- let envelope = order_settlement_decision_from_event(event)?;
- Ok(RadrootsOrderEventRecord::Settlement(
- RadrootsOrderSettlementRecord {
- event_id,
- author_pubkey,
- counterparty_pubkey: context.counterparty_pubkey.clone(),
- root_event_id: require_context_root_event_id(&context)?,
- prev_event_id: require_context_prev_event_id(&context)?,
- payload: envelope.payload,
- },
- ))
- }
_ => Err(RadrootsOrderEventDecodeError::UnsupportedKind { kind: event.kind }),
}
}
@@ -491,26 +377,6 @@ pub enum RadrootsOrderStatus {
Accepted,
Declined,
Cancelled,
- Completed,
- Disputed,
- Invalid,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsOrderPaymentState {
- NotRecorded,
- Recorded,
- Settled,
- Rejected,
- Invalid,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsOrderSettlementState {
- NotRequired,
- Pending,
- Accepted,
- Rejected,
Invalid,
}
@@ -537,7 +403,6 @@ pub enum RadrootsOrderIssue {
DecisionInventoryCommitmentMismatch { event_id: RadrootsEventId },
DecisionMissingReason { event_id: RadrootsEventId },
ConflictingDecisions { event_ids: Vec<RadrootsEventId> },
- RevisionProposalWithoutAcceptedDecision { event_id: RadrootsEventId },
RevisionProposalPayloadInvalid { event_id: RadrootsEventId },
RevisionProposalOrderIdMismatch { event_id: RadrootsEventId },
RevisionProposalAuthorMismatch { event_id: RadrootsEventId },
@@ -560,20 +425,6 @@ pub enum RadrootsOrderIssue {
RevisionDecisionRootMismatch { event_id: RadrootsEventId },
RevisionDecisionPreviousMismatch { event_id: RadrootsEventId },
RevisionDecisionRevisionIdMismatch { event_id: RadrootsEventId },
- FulfillmentWithoutAcceptedDecision { event_id: RadrootsEventId },
- FulfillmentPayloadInvalid { event_id: RadrootsEventId },
- FulfillmentOrderIdMismatch { event_id: RadrootsEventId },
- FulfillmentAuthorMismatch { event_id: RadrootsEventId },
- FulfillmentCounterpartyMismatch { event_id: RadrootsEventId },
- FulfillmentBuyerMismatch { event_id: RadrootsEventId },
- FulfillmentSellerMismatch { event_id: RadrootsEventId },
- FulfillmentListingAddressInvalid { event_id: RadrootsEventId },
- FulfillmentListingMismatch { event_id: RadrootsEventId },
- FulfillmentRootMismatch { event_id: RadrootsEventId },
- FulfillmentPreviousMismatch { event_id: RadrootsEventId },
- FulfillmentStatusNotPublishable { event_id: RadrootsEventId },
- FulfillmentUnsupportedTransition { event_id: RadrootsEventId },
- ForkedFulfillments { event_ids: Vec<RadrootsEventId> },
CancellationWithoutCancellableOrder { event_id: RadrootsEventId },
CancellationPayloadInvalid { event_id: RadrootsEventId },
CancellationOrderIdMismatch { event_id: RadrootsEventId },
@@ -585,121 +436,17 @@ pub enum RadrootsOrderIssue {
CancellationListingMismatch { event_id: RadrootsEventId },
CancellationRootMismatch { event_id: RadrootsEventId },
CancellationPreviousMismatch { event_id: RadrootsEventId },
- CancellationAfterFulfillment { event_id: RadrootsEventId },
- ReceiptWithoutEligibleFulfillment { event_id: RadrootsEventId },
- ReceiptPayloadInvalid { event_id: RadrootsEventId },
- ReceiptOrderIdMismatch { event_id: RadrootsEventId },
- ReceiptAuthorMismatch { event_id: RadrootsEventId },
- ReceiptCounterpartyMismatch { event_id: RadrootsEventId },
- ReceiptBuyerMismatch { event_id: RadrootsEventId },
- ReceiptSellerMismatch { event_id: RadrootsEventId },
- ReceiptListingAddressInvalid { event_id: RadrootsEventId },
- ReceiptListingMismatch { event_id: RadrootsEventId },
- ReceiptRootMismatch { event_id: RadrootsEventId },
- ReceiptPreviousMismatch { event_id: RadrootsEventId },
- PaymentWithoutAcceptedAgreement { event_id: RadrootsEventId },
- PaymentPayloadInvalid { event_id: RadrootsEventId },
- PaymentOrderIdMismatch { event_id: RadrootsEventId },
- PaymentAuthorMismatch { event_id: RadrootsEventId },
- PaymentCounterpartyMismatch { event_id: RadrootsEventId },
- PaymentBuyerMismatch { event_id: RadrootsEventId },
- PaymentSellerMismatch { event_id: RadrootsEventId },
- PaymentListingAddressInvalid { event_id: RadrootsEventId },
- PaymentListingMismatch { event_id: RadrootsEventId },
- PaymentRootMismatch { event_id: RadrootsEventId },
- PaymentPreviousMismatch { event_id: RadrootsEventId },
- PaymentAgreementMismatch { event_id: RadrootsEventId },
- PaymentQuoteMismatch { event_id: RadrootsEventId },
- PaymentQuoteVersionMismatch { event_id: RadrootsEventId },
- PaymentEconomicsDigestMismatch { event_id: RadrootsEventId },
- PaymentAmountMismatch { event_id: RadrootsEventId },
- PaymentCurrencyMismatch { event_id: RadrootsEventId },
- PaymentAfterCancellation { event_id: RadrootsEventId },
- RevisionAfterPayment { event_id: RadrootsEventId },
- DuplicatePayments { event_ids: Vec<RadrootsEventId> },
- SettlementWithoutValidPayment { event_id: RadrootsEventId },
- SettlementPayloadInvalid { event_id: RadrootsEventId },
- SettlementOrderIdMismatch { event_id: RadrootsEventId },
- SettlementAuthorMismatch { event_id: RadrootsEventId },
- SettlementCounterpartyMismatch { event_id: RadrootsEventId },
- SettlementBuyerMismatch { event_id: RadrootsEventId },
- SettlementSellerMismatch { event_id: RadrootsEventId },
- SettlementListingAddressInvalid { event_id: RadrootsEventId },
- SettlementListingMismatch { event_id: RadrootsEventId },
- SettlementRootMismatch { event_id: RadrootsEventId },
- SettlementPreviousMismatch { event_id: RadrootsEventId },
- SettlementPaymentEventMismatch { event_id: RadrootsEventId },
- SettlementAgreementMismatch { event_id: RadrootsEventId },
- SettlementQuoteMismatch { event_id: RadrootsEventId },
- SettlementQuoteVersionMismatch { event_id: RadrootsEventId },
- SettlementEconomicsDigestMismatch { event_id: RadrootsEventId },
- SettlementAmountMismatch { event_id: RadrootsEventId },
- SettlementCurrencyMismatch { event_id: RadrootsEventId },
- DuplicateSettlements { event_ids: Vec<RadrootsEventId> },
ForkedLifecycle { event_ids: Vec<RadrootsEventId> },
}
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderPaymentProjection {
- pub state: RadrootsOrderPaymentState,
- pub settlement_state: RadrootsOrderSettlementState,
- pub payment_event_id: Option<RadrootsEventId>,
- pub settlement_event_id: Option<RadrootsEventId>,
- pub agreement_event_id: Option<RadrootsEventId>,
- pub quote_id: Option<RadrootsOrderQuoteId>,
- pub quote_version: Option<u32>,
- pub economics_digest: Option<RadrootsEconomicsDigest>,
- pub amount: Option<RadrootsCoreDecimal>,
- pub currency: Option<RadrootsCoreCurrency>,
- pub method: Option<RadrootsOrderPaymentMethod>,
- pub reference: Option<String>,
- pub paid_at: Option<u64>,
- pub reason: Option<String>,
-}
-
-impl RadrootsOrderPaymentProjection {
- pub fn not_recorded() -> Self {
- Self {
- state: RadrootsOrderPaymentState::NotRecorded,
- settlement_state: RadrootsOrderSettlementState::NotRequired,
- payment_event_id: None,
- settlement_event_id: None,
- agreement_event_id: None,
- quote_id: None,
- quote_version: None,
- economics_digest: None,
- amount: None,
- currency: None,
- method: None,
- reference: None,
- paid_at: None,
- reason: None,
- }
- }
-
- pub fn invalid() -> Self {
- let mut projection = Self::not_recorded();
- projection.state = RadrootsOrderPaymentState::Invalid;
- projection.settlement_state = RadrootsOrderSettlementState::Invalid;
- projection
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsOrderProjection {
pub order_id: RadrootsOrderId,
pub status: RadrootsOrderStatus,
pub request_event_id: Option<RadrootsEventId>,
pub decision_event_id: Option<RadrootsEventId>,
- pub fulfillment_event_id: Option<RadrootsEventId>,
- pub fulfillment_status: Option<RadrootsOrderFulfillmentState>,
pub cancellation_event_id: Option<RadrootsEventId>,
- pub receipt_event_id: Option<RadrootsEventId>,
- pub receipt_received: Option<bool>,
- pub receipt_issue: Option<String>,
- pub receipt_received_at: Option<u64>,
pub lifecycle_terminal: bool,
- pub payment: RadrootsOrderPaymentProjection,
pub economics: Option<RadrootsOrderEconomics>,
pub agreement_event_id: Option<RadrootsEventId>,
pub pending_revision_event_id: Option<RadrootsEventId>,
@@ -726,7 +473,7 @@ pub struct RadrootsListingInventoryBinAvailability {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsListingInventoryOrderReservation {
pub order_id: RadrootsOrderId,
- pub decision_event_id: RadrootsEventId,
+ pub agreement_event_id: RadrootsEventId,
pub bin_count: u64,
}
@@ -774,16 +521,12 @@ pub struct RadrootsListingInventoryAccountingProjection {
}
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOrderReductionInputs<I, J, K, L, M, N, O, P, Q> {
+pub struct RadrootsOrderReductionInputs<I, J, K, L, M> {
pub requests: I,
pub decisions: J,
pub revision_proposals: K,
pub revision_decisions: L,
- pub fulfillments: M,
- pub cancellations: N,
- pub receipts: O,
- pub payments: P,
- pub settlements: Q,
+ pub cancellations: M,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@@ -792,23 +535,17 @@ pub struct RadrootsGroupedOrderEventRecords {
pub decisions: Vec<RadrootsOrderDecisionRecord>,
pub revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>,
pub revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>,
- pub fulfillments: Vec<RadrootsOrderFulfillmentRecord>,
pub cancellations: Vec<RadrootsOrderCancellationRecord>,
- pub receipts: Vec<RadrootsOrderReceiptRecord>,
- pub payments: Vec<RadrootsOrderPaymentEventRecord>,
- pub settlements: Vec<RadrootsOrderSettlementRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsListingInventoryAccountingInputs<I, J, K, L, M, N, O, P> {
+pub struct RadrootsListingInventoryAccountingInputs<I, J, K, L, M, N> {
pub bins: I,
pub requests: J,
pub decisions: K,
pub revision_proposals: L,
pub revision_decisions: M,
- pub fulfillments: N,
- pub cancellations: O,
- pub receipts: P,
+ pub cancellations: N,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@@ -818,49 +555,20 @@ struct RadrootsListingInventoryAccountingRecords {
decisions: Vec<RadrootsOrderDecisionRecord>,
revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>,
revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>,
- fulfillments: Vec<RadrootsOrderFulfillmentRecord>,
- cancellations: Vec<RadrootsOrderCancellationRecord>,
- receipts: Vec<RadrootsOrderReceiptRecord>,
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-struct RadrootsOrderDecisionProjectionRecords {
- revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>,
- revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>,
- fulfillments: Vec<RadrootsOrderFulfillmentRecord>,
cancellations: Vec<RadrootsOrderCancellationRecord>,
- receipts: Vec<RadrootsOrderReceiptRecord>,
- payments: Vec<RadrootsOrderPaymentEventRecord>,
- settlements: Vec<RadrootsOrderSettlementRecord>,
-}
-
-struct RadrootsReceiptProjectionInput<'a> {
- order_id: &'a RadrootsOrderId,
- request: &'a RadrootsOrderRequestRecord,
- decision: &'a RadrootsOrderDecisionRecord,
- agreement_event_id: &'a RadrootsEventId,
- economics: &'a RadrootsOrderEconomics,
- latest_fulfillment: Option<&'a RadrootsOrderFulfillmentRecord>,
- fulfillments: &'a [RadrootsOrderFulfillmentRecord],
- receipts: Vec<RadrootsOrderReceiptRecord>,
- issues: &'a mut Vec<RadrootsOrderIssue>,
}
#[cfg_attr(coverage_nightly, coverage(off))]
-pub fn reduce_order_events<I, J, K, L, M, N, O, P, Q>(
+pub fn reduce_order_events<I, J, K, L, M>(
order_id: &RadrootsOrderId,
- inputs: RadrootsOrderReductionInputs<I, J, K, L, M, N, O, P, Q>,
+ inputs: RadrootsOrderReductionInputs<I, J, K, L, M>,
) -> RadrootsOrderProjection
where
I: IntoIterator<Item = RadrootsOrderRequestRecord>,
J: IntoIterator<Item = RadrootsOrderDecisionRecord>,
K: IntoIterator<Item = RadrootsOrderRevisionProposalRecord>,
L: IntoIterator<Item = RadrootsOrderRevisionDecisionRecord>,
- M: IntoIterator<Item = RadrootsOrderFulfillmentRecord>,
- N: IntoIterator<Item = RadrootsOrderCancellationRecord>,
- O: IntoIterator<Item = RadrootsOrderReceiptRecord>,
- P: IntoIterator<Item = RadrootsOrderPaymentEventRecord>,
- Q: IntoIterator<Item = RadrootsOrderSettlementRecord>,
+ M: IntoIterator<Item = RadrootsOrderCancellationRecord>,
{
reduce_grouped_order_event_records(
order_id,
@@ -869,11 +577,7 @@ where
decisions: inputs.decisions.into_iter().collect(),
revision_proposals: inputs.revision_proposals.into_iter().collect(),
revision_decisions: inputs.revision_decisions.into_iter().collect(),
- fulfillments: inputs.fulfillments.into_iter().collect(),
cancellations: inputs.cancellations.into_iter().collect(),
- receipts: inputs.receipts.into_iter().collect(),
- payments: inputs.payments.into_iter().collect(),
- settlements: inputs.settlements.into_iter().collect(),
},
)
}
@@ -887,15 +591,7 @@ where
I: IntoIterator<Item = RadrootsOrderEventRecord>,
{
let mut seen_event_ids = Vec::new();
- let mut requests = Vec::new();
- let mut decisions = Vec::new();
- let mut revision_proposals = Vec::new();
- let mut revision_decisions = Vec::new();
- let mut fulfillments = Vec::new();
- let mut cancellations = Vec::new();
- let mut receipts = Vec::new();
- let mut payments = Vec::new();
- let mut settlements = Vec::new();
+ let mut grouped = RadrootsGroupedOrderEventRecords::default();
for record in records {
let event_id = record.event_id().clone();
@@ -904,32 +600,19 @@ where
}
seen_event_ids.push(event_id);
match record {
- RadrootsOrderEventRecord::Request(record) => requests.push(record),
- RadrootsOrderEventRecord::Decision(record) => decisions.push(record),
- RadrootsOrderEventRecord::RevisionProposal(record) => revision_proposals.push(record),
- RadrootsOrderEventRecord::RevisionDecision(record) => revision_decisions.push(record),
- RadrootsOrderEventRecord::Fulfillment(record) => fulfillments.push(record),
- RadrootsOrderEventRecord::Cancellation(record) => cancellations.push(record),
- RadrootsOrderEventRecord::Receipt(record) => receipts.push(record),
- RadrootsOrderEventRecord::Payment(record) => payments.push(record),
- RadrootsOrderEventRecord::Settlement(record) => settlements.push(record),
+ RadrootsOrderEventRecord::Request(record) => grouped.requests.push(record),
+ RadrootsOrderEventRecord::Decision(record) => grouped.decisions.push(record),
+ RadrootsOrderEventRecord::RevisionProposal(record) => {
+ grouped.revision_proposals.push(record);
+ }
+ RadrootsOrderEventRecord::RevisionDecision(record) => {
+ grouped.revision_decisions.push(record);
+ }
+ RadrootsOrderEventRecord::Cancellation(record) => grouped.cancellations.push(record),
}
}
- reduce_grouped_order_event_records(
- order_id,
- RadrootsGroupedOrderEventRecords {
- requests,
- decisions,
- revision_proposals,
- revision_decisions,
- fulfillments,
- cancellations,
- receipts,
- payments,
- settlements,
- },
- )
+ reduce_grouped_order_event_records(order_id, grouped)
}
fn reduce_grouped_order_event_records(
@@ -940,44 +623,14 @@ fn reduce_grouped_order_event_records(
let decisions = unique_decision_records(records.decisions);
let revision_proposals = unique_revision_proposal_records(records.revision_proposals);
let revision_decisions = unique_revision_decision_records(records.revision_decisions);
- let fulfillments = unique_fulfillment_records(records.fulfillments);
let cancellations = unique_cancellation_records(records.cancellations);
- let receipts = unique_receipt_records(records.receipts);
- let payments = unique_payment_records(records.payments);
- let settlements = unique_settlement_records(records.settlements);
if requests.is_empty()
&& decisions.is_empty()
&& revision_proposals.is_empty()
&& revision_decisions.is_empty()
- && fulfillments.is_empty()
&& cancellations.is_empty()
- && receipts.is_empty()
- && payments.is_empty()
- && settlements.is_empty()
{
- return RadrootsOrderProjection {
- order_id: order_id.clone(),
- status: RadrootsOrderStatus::Missing,
- request_event_id: None,
- decision_event_id: None,
- fulfillment_event_id: None,
- fulfillment_status: None,
- cancellation_event_id: None,
- receipt_event_id: None,
- receipt_received: None,
- receipt_issue: None,
- receipt_received_at: None,
- lifecycle_terminal: false,
- payment: RadrootsOrderPaymentProjection::not_recorded(),
- economics: None,
- agreement_event_id: None,
- pending_revision_event_id: None,
- listing_addr: None,
- buyer_pubkey: None,
- seller_pubkey: None,
- last_event_id: None,
- issues: Vec::new(),
- };
+ return empty_projection(order_id, RadrootsOrderStatus::Missing, false);
}
let mut issues = Vec::new();
@@ -993,23 +646,18 @@ fn reduce_grouped_order_event_records(
.iter()
.map(|request| request.event_id.clone())
.collect::<Vec<_>>();
- event_ids.sort();
+ sort_and_dedup_values(&mut event_ids);
issues.push(RadrootsOrderIssue::MultipleRequests { event_ids });
}
let Some(request) = valid_requests.first() else {
- if decisions.is_empty()
- && revision_proposals.is_empty()
- && revision_decisions.is_empty()
- && fulfillments.is_empty()
- && cancellations.is_empty()
- && receipts.is_empty()
- && payments.is_empty()
- && settlements.is_empty()
+ if !decisions.is_empty()
+ || !revision_proposals.is_empty()
+ || !revision_decisions.is_empty()
+ || !cancellations.is_empty()
{
- return invalid_projection(order_id, None, issues);
+ issues.push(RadrootsOrderIssue::MissingRequest);
}
- issues.push(RadrootsOrderIssue::MissingRequest);
return invalid_projection(order_id, None, issues);
};
@@ -1038,40 +686,22 @@ fn reduce_grouped_order_event_records(
}
}
- if !issues.is_empty() {
- return invalid_projection(order_id, Some(request), issues);
- }
-
let mut valid_cancellations = Vec::new();
for cancellation in cancellations {
if validate_order_cancellation_record(request, &cancellation, &mut issues) {
valid_cancellations.push(cancellation);
}
}
- let mut valid_receipts = Vec::new();
- for receipt in receipts {
- if validate_order_receipt_record(request, &receipt, &mut issues) {
- valid_receipts.push(receipt);
- }
- }
+
if !issues.is_empty() {
return invalid_projection(order_id, Some(request), issues);
}
- let request_cancellations = valid_cancellations
- .iter()
- .filter(|cancellation| cancellation.prev_event_id == request.event_id)
- .collect::<Vec<_>>();
- if !request_cancellations.is_empty() && !valid_decisions.is_empty() {
- let mut event_ids = valid_decisions
+ if valid_cancellations.len() > 1 {
+ let mut event_ids = valid_cancellations
.iter()
- .map(|decision| decision.event_id.clone())
+ .map(|cancellation| cancellation.event_id.clone())
.collect::<Vec<_>>();
- event_ids.extend(
- request_cancellations
- .iter()
- .map(|cancellation| cancellation.event_id.clone()),
- );
sort_and_dedup_values(&mut event_ids);
return invalid_projection(
order_id,
@@ -1080,54 +710,37 @@ fn reduce_grouped_order_event_records(
);
}
+ if let Some(cancellation) = valid_cancellations.first() {
+ return cancelled_projection(
+ order_id,
+ request,
+ cancellation,
+ &valid_decisions,
+ &valid_revision_proposals,
+ &valid_revision_decisions,
+ );
+ }
+
match valid_decisions.len() {
- 0 => {
- record_revision_proposal_without_accepted_decision(
- &valid_revision_proposals,
- &mut issues,
- );
- record_revision_decision_without_proposal(&valid_revision_decisions, &mut issues);
- if !fulfillments.is_empty() {
- record_fulfillment_without_accepted_decision(&fulfillments, &mut issues);
- }
- if !valid_receipts.is_empty() {
- record_receipt_without_eligible_fulfillment(&valid_receipts, &mut issues);
- }
- record_payment_without_accepted_agreement(&payments, &mut issues);
- record_settlement_without_valid_payment(&settlements, &mut issues);
- if !issues.is_empty() {
- invalid_projection_with_payment(
- order_id,
- Some(request),
- issues,
- RadrootsOrderPaymentProjection::invalid(),
- )
- } else if valid_cancellations.is_empty() {
- requested_projection(order_id, request)
- } else {
- requested_cancellation_projection(order_id, request, valid_cancellations)
- }
- }
+ 0 => negotiation_projection(
+ order_id,
+ request,
+ &valid_revision_proposals,
+ &valid_revision_decisions,
+ ),
1 => decided_projection(
order_id,
request,
&valid_decisions[0],
- RadrootsOrderDecisionProjectionRecords {
- revision_proposals: valid_revision_proposals,
- revision_decisions: valid_revision_decisions,
- fulfillments,
- cancellations: valid_cancellations,
- receipts: valid_receipts,
- payments,
- settlements,
- },
+ &valid_revision_proposals,
+ &valid_revision_decisions,
),
_ => {
let mut event_ids = valid_decisions
.iter()
.map(|decision| decision.event_id.clone())
.collect::<Vec<_>>();
- event_ids.sort();
+ sort_and_dedup_values(&mut event_ids);
invalid_projection(
order_id,
Some(request),
@@ -1138,10 +751,10 @@ fn reduce_grouped_order_event_records(
}
#[cfg_attr(coverage_nightly, coverage(off))]
-pub fn reduce_listing_inventory_accounting<I, J, K, L, M, N, O, P>(
+pub fn reduce_listing_inventory_accounting<I, J, K, L, M, N>(
listing_addr: &RadrootsListingAddress,
listing_event_id: &RadrootsEventId,
- inputs: RadrootsListingInventoryAccountingInputs<I, J, K, L, M, N, O, P>,
+ inputs: RadrootsListingInventoryAccountingInputs<I, J, K, L, M, N>,
) -> RadrootsListingInventoryAccountingProjection
where
I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>,
@@ -1149,9 +762,7 @@ where
K: IntoIterator<Item = RadrootsOrderDecisionRecord>,
L: IntoIterator<Item = RadrootsOrderRevisionProposalRecord>,
M: IntoIterator<Item = RadrootsOrderRevisionDecisionRecord>,
- N: IntoIterator<Item = RadrootsOrderFulfillmentRecord>,
- O: IntoIterator<Item = RadrootsOrderCancellationRecord>,
- P: IntoIterator<Item = RadrootsOrderReceiptRecord>,
+ N: IntoIterator<Item = RadrootsOrderCancellationRecord>,
{
reduce_listing_inventory_accounting_records(
listing_addr,
@@ -1162,9 +773,7 @@ where
decisions: inputs.decisions.into_iter().collect(),
revision_proposals: inputs.revision_proposals.into_iter().collect(),
revision_decisions: inputs.revision_decisions.into_iter().collect(),
- fulfillments: inputs.fulfillments.into_iter().collect(),
cancellations: inputs.cancellations.into_iter().collect(),
- receipts: inputs.receipts.into_iter().collect(),
},
)
}
@@ -1191,26 +800,16 @@ fn reduce_listing_inventory_accounting_records(
.into_iter()
.filter(|decision| decision.payload.listing_addr.as_str() == listing_addr.as_str())
.collect::<Vec<_>>();
- let fulfillments = unique_fulfillment_records(records.fulfillments)
- .into_iter()
- .filter(|fulfillment| fulfillment.payload.listing_addr.as_str() == listing_addr.as_str())
- .collect::<Vec<_>>();
let cancellations = unique_cancellation_records(records.cancellations)
.into_iter()
.filter(|cancellation| cancellation.payload.listing_addr.as_str() == listing_addr.as_str())
.collect::<Vec<_>>();
- let receipts = unique_receipt_records(records.receipts)
- .into_iter()
- .filter(|receipt| receipt.payload.listing_addr.as_str() == listing_addr.as_str())
- .collect::<Vec<_>>();
let mut order_ids = listing_order_ids(
&requests,
&decisions,
&revision_proposals,
&revision_decisions,
- &fulfillments,
&cancellations,
- &receipts,
);
let mut declined_order_ids = Vec::new();
let mut cancelled_order_ids = Vec::new();
@@ -1227,11 +826,6 @@ fn reduce_listing_inventory_accounting_records(
.filter(|decision| decision.payload.order_id == order_id)
.cloned()
.collect::<Vec<_>>();
- let order_fulfillments = fulfillments
- .iter()
- .filter(|fulfillment| fulfillment.payload.order_id == order_id)
- .cloned()
- .collect::<Vec<_>>();
let order_revision_proposals = revision_proposals
.iter()
.filter(|proposal| proposal.payload.order_id == order_id)
@@ -1247,11 +841,6 @@ fn reduce_listing_inventory_accounting_records(
.filter(|cancellation| cancellation.payload.order_id == order_id)
.cloned()
.collect::<Vec<_>>();
- let order_receipts = receipts
- .iter()
- .filter(|receipt| receipt.payload.order_id == order_id)
- .cloned()
- .collect::<Vec<_>>();
let projection = reduce_order_events(
&order_id,
RadrootsOrderReductionInputs {
@@ -1259,22 +848,11 @@ fn reduce_listing_inventory_accounting_records(
decisions: order_decisions.clone(),
revision_proposals: order_revision_proposals.clone(),
revision_decisions: order_revision_decisions.clone(),
- fulfillments: order_fulfillments.clone(),
cancellations: order_cancellations.clone(),
- receipts: order_receipts.clone(),
- payments: Vec::<RadrootsOrderPaymentEventRecord>::new(),
- settlements: Vec::<RadrootsOrderSettlementRecord>::new(),
},
);
match projection.status {
- RadrootsOrderStatus::Accepted
- | RadrootsOrderStatus::Completed
- | RadrootsOrderStatus::Disputed => {
- if projection.fulfillment_status
- == Some(RadrootsOrderFulfillmentState::SellerCancelled)
- {
- continue;
- }
+ RadrootsOrderStatus::Accepted => {
if let Some(agreement_event_id) = projection.agreement_event_id.as_ref()
&& let Some(economics) = projection.economics.as_ref()
{
@@ -1313,20 +891,10 @@ fn reduce_listing_inventory_accounting_records(
.map(|decision| decision.event_id.clone()),
);
event_ids.extend(
- order_fulfillments
- .iter()
- .map(|fulfillment| fulfillment.event_id.clone()),
- );
- event_ids.extend(
order_cancellations
.iter()
.map(|cancellation| cancellation.event_id.clone()),
);
- event_ids.extend(
- order_receipts
- .iter()
- .map(|receipt| receipt.event_id.clone()),
- );
sort_and_dedup_values(&mut event_ids);
}
invalid_event_ids.extend(event_ids.iter().cloned());
@@ -1416,536 +984,308 @@ pub fn radroots_order_economics_digest(
Ok(value)
}
-fn unique_request_records(
- requests: Vec<RadrootsOrderRequestRecord>,
-) -> Vec<RadrootsOrderRequestRecord> {
- let mut unique = Vec::new();
- let mut records = requests;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for request in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderRequestRecord| existing.event_id != request.event_id)
- {
- unique.push(request);
- }
+fn cancelled_projection(
+ order_id: &RadrootsOrderId,
+ request: &RadrootsOrderRequestRecord,
+ cancellation: &RadrootsOrderCancellationRecord,
+ decisions: &[RadrootsOrderDecisionRecord],
+ revision_proposals: &[RadrootsOrderRevisionProposalRecord],
+ revision_decisions: &[RadrootsOrderRevisionDecisionRecord],
+) -> RadrootsOrderProjection {
+ if !decisions.is_empty() || !revision_decisions.is_empty() {
+ let mut event_ids = Vec::new();
+ event_ids.extend(decisions.iter().map(|decision| decision.event_id.clone()));
+ event_ids.extend(
+ revision_decisions
+ .iter()
+ .map(|decision| decision.event_id.clone()),
+ );
+ event_ids.push(cancellation.event_id.clone());
+ sort_and_dedup_values(&mut event_ids);
+ return invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }],
+ );
}
- unique
-}
-
-fn unique_decision_records(
- decisions: Vec<RadrootsOrderDecisionRecord>,
-) -> Vec<RadrootsOrderDecisionRecord> {
- let mut unique = Vec::new();
- let mut records = decisions;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for decision in records {
- if unique
+ if revision_proposals.len() > 1 {
+ let mut event_ids = revision_proposals
.iter()
- .all(|existing: &RadrootsOrderDecisionRecord| existing.event_id != decision.event_id)
- {
- unique.push(decision);
- }
+ .map(|proposal| proposal.event_id.clone())
+ .collect::<Vec<_>>();
+ event_ids.push(cancellation.event_id.clone());
+ sort_and_dedup_values(&mut event_ids);
+ return invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }],
+ );
}
- unique
+ let expected_prev_event_id = revision_proposals
+ .first()
+ .map(|proposal| &proposal.event_id)
+ .unwrap_or(&request.event_id);
+ if &cancellation.prev_event_id != expected_prev_event_id {
+ return invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::CancellationPreviousMismatch {
+ event_id: cancellation.event_id.clone(),
+ }],
+ );
+ }
+
+ let mut projection = request_projection(order_id, request, RadrootsOrderStatus::Cancelled);
+ projection.cancellation_event_id = Some(cancellation.event_id.clone());
+ projection.lifecycle_terminal = true;
+ projection.last_event_id = Some(cancellation.event_id.clone());
+ projection
}
-fn unique_revision_proposal_records(
- revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>,
-) -> Vec<RadrootsOrderRevisionProposalRecord> {
- let mut unique = Vec::new();
- let mut records = revision_proposals;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for proposal in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderRevisionProposalRecord| {
- existing.event_id != proposal.event_id
- })
- {
- unique.push(proposal);
+fn negotiation_projection(
+ order_id: &RadrootsOrderId,
+ request: &RadrootsOrderRequestRecord,
+ revision_proposals: &[RadrootsOrderRevisionProposalRecord],
+ revision_decisions: &[RadrootsOrderRevisionDecisionRecord],
+) -> RadrootsOrderProjection {
+ match revision_proposals.len() {
+ 0 => {
+ if revision_decisions.is_empty() {
+ request_projection(order_id, request, RadrootsOrderStatus::Requested)
+ } else {
+ invalid_projection(
+ order_id,
+ Some(request),
+ revision_decisions
+ .iter()
+ .map(
+ |decision| RadrootsOrderIssue::RevisionDecisionWithoutProposal {
+ event_id: decision.event_id.clone(),
+ },
+ )
+ .collect(),
+ )
+ }
+ }
+ 1 => {
+ let proposal = &revision_proposals[0];
+ if proposal.prev_event_id != request.event_id {
+ return invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::RevisionProposalPreviousMismatch {
+ event_id: proposal.event_id.clone(),
+ }],
+ );
+ }
+ match revision_decisions.len() {
+ 0 => {
+ let mut projection =
+ request_projection(order_id, request, RadrootsOrderStatus::Requested);
+ projection.pending_revision_event_id = Some(proposal.event_id.clone());
+ projection.economics = Some(proposal.payload.economics.clone());
+ projection.last_event_id = Some(proposal.event_id.clone());
+ projection
+ }
+ 1 => revision_decision_projection(
+ order_id,
+ request,
+ proposal,
+ &revision_decisions[0],
+ ),
+ _ => {
+ let mut event_ids = revision_decisions
+ .iter()
+ .map(|decision| decision.event_id.clone())
+ .collect::<Vec<_>>();
+ sort_and_dedup_values(&mut event_ids);
+ invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }],
+ )
+ }
+ }
+ }
+ _ => {
+ let mut event_ids = revision_proposals
+ .iter()
+ .map(|proposal| proposal.event_id.clone())
+ .collect::<Vec<_>>();
+ sort_and_dedup_values(&mut event_ids);
+ invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }],
+ )
}
}
- unique
}
-fn unique_revision_decision_records(
- revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>,
-) -> Vec<RadrootsOrderRevisionDecisionRecord> {
- let mut unique = Vec::new();
- let mut records = revision_decisions;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for decision in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderRevisionDecisionRecord| {
- existing.event_id != decision.event_id
- })
- {
- unique.push(decision);
- }
+fn decided_projection(
+ order_id: &RadrootsOrderId,
+ request: &RadrootsOrderRequestRecord,
+ decision: &RadrootsOrderDecisionRecord,
+ revision_proposals: &[RadrootsOrderRevisionProposalRecord],
+ revision_decisions: &[RadrootsOrderRevisionDecisionRecord],
+) -> RadrootsOrderProjection {
+ if !revision_proposals.is_empty() || !revision_decisions.is_empty() {
+ let mut event_ids = Vec::new();
+ event_ids.extend(
+ revision_proposals
+ .iter()
+ .map(|proposal| proposal.event_id.clone()),
+ );
+ event_ids.extend(
+ revision_decisions
+ .iter()
+ .map(|decision| decision.event_id.clone()),
+ );
+ event_ids.push(decision.event_id.clone());
+ sort_and_dedup_values(&mut event_ids);
+ return invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }],
+ );
}
- unique
-}
-fn unique_fulfillment_records(
- fulfillments: Vec<RadrootsOrderFulfillmentRecord>,
-) -> Vec<RadrootsOrderFulfillmentRecord> {
- let mut unique = Vec::new();
- let mut records = fulfillments;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for fulfillment in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderFulfillmentRecord| {
- existing.event_id != fulfillment.event_id
- })
- {
- unique.push(fulfillment);
+ match &decision.payload.decision {
+ RadrootsOrderDecisionOutcome::Accepted { .. } => {
+ let mut projection =
+ request_projection(order_id, request, RadrootsOrderStatus::Accepted);
+ projection.decision_event_id = Some(decision.event_id.clone());
+ projection.lifecycle_terminal = true;
+ projection.economics = Some(request.payload.economics.clone());
+ projection.agreement_event_id = Some(decision.event_id.clone());
+ projection.last_event_id = Some(decision.event_id.clone());
+ projection
}
- }
- unique
-}
-
-fn unique_cancellation_records(
- cancellations: Vec<RadrootsOrderCancellationRecord>,
-) -> Vec<RadrootsOrderCancellationRecord> {
- let mut unique = Vec::new();
- let mut records = cancellations;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for cancellation in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderCancellationRecord| {
- existing.event_id != cancellation.event_id
- })
- {
- unique.push(cancellation);
+ RadrootsOrderDecisionOutcome::Declined { .. } => {
+ let mut projection =
+ request_projection(order_id, request, RadrootsOrderStatus::Declined);
+ projection.decision_event_id = Some(decision.event_id.clone());
+ projection.lifecycle_terminal = true;
+ projection.last_event_id = Some(decision.event_id.clone());
+ projection
}
}
- unique
}
-fn unique_receipt_records(
- receipts: Vec<RadrootsOrderReceiptRecord>,
-) -> Vec<RadrootsOrderReceiptRecord> {
- let mut unique = Vec::new();
- let mut records = receipts;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for receipt in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderReceiptRecord| existing.event_id != receipt.event_id)
- {
- unique.push(receipt);
- }
+fn revision_decision_projection(
+ order_id: &RadrootsOrderId,
+ request: &RadrootsOrderRequestRecord,
+ proposal: &RadrootsOrderRevisionProposalRecord,
+ decision: &RadrootsOrderRevisionDecisionRecord,
+) -> RadrootsOrderProjection {
+ if decision.prev_event_id != proposal.event_id {
+ return invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::RevisionDecisionPreviousMismatch {
+ event_id: decision.event_id.clone(),
+ }],
+ );
}
- unique
-}
-
-fn unique_payment_records(
- payments: Vec<RadrootsOrderPaymentEventRecord>,
-) -> Vec<RadrootsOrderPaymentEventRecord> {
- let mut unique = Vec::new();
- let mut records = payments;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for payment in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderPaymentEventRecord| existing.event_id != payment.event_id)
- {
- unique.push(payment);
- }
+ if decision.payload.revision_id != proposal.payload.revision_id {
+ return invalid_projection(
+ order_id,
+ Some(request),
+ vec![RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch {
+ event_id: decision.event_id.clone(),
+ }],
+ );
}
- unique
-}
-fn unique_settlement_records(
- settlements: Vec<RadrootsOrderSettlementRecord>,
-) -> Vec<RadrootsOrderSettlementRecord> {
- let mut unique = Vec::new();
- let mut records = settlements;
- records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- for settlement in records {
- if unique
- .iter()
- .all(|existing: &RadrootsOrderSettlementRecord| {
- existing.event_id != settlement.event_id
- })
- {
- unique.push(settlement);
+ match &decision.payload.decision {
+ RadrootsOrderRevisionOutcome::Accepted => {
+ let mut projection =
+ request_projection(order_id, request, RadrootsOrderStatus::Accepted);
+ projection.economics = Some(proposal.payload.economics.clone());
+ projection.agreement_event_id = Some(decision.event_id.clone());
+ projection.lifecycle_terminal = true;
+ projection.last_event_id = Some(decision.event_id.clone());
+ projection
}
- }
- unique
-}
-
-fn normalized_listing_inventory_bins<I>(
- bins: I,
-) -> (
- Vec<RadrootsListingInventoryBinAccounting>,
- Vec<RadrootsListingInventoryAccountingIssue>,
-)
-where
- I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>,
-{
- let mut normalized: Vec<RadrootsListingInventoryBinAccounting> = Vec::new();
- let mut issues = Vec::new();
- for bin in bins {
- let bin_id = bin.bin_id;
- if let Some(existing) = normalized
- .iter_mut()
- .find(|existing| existing.bin_id == bin_id)
- {
- if let Some(next_count) = existing.available_count.checked_add(bin.available_count) {
- existing.available_count = next_count;
- existing.remaining_count = next_count;
- } else {
- existing.available_count = u64::MAX;
- existing.remaining_count = u64::MAX;
- issues.push(
- RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
- bin_id: existing.bin_id.clone(),
- event_ids: Vec::new(),
- },
- );
- }
- } else {
- normalized.push(RadrootsListingInventoryBinAccounting {
- bin_id,
- available_count: bin.available_count,
- accepted_reserved_count: 0,
- remaining_count: bin.available_count,
- over_reserved: false,
- accepted_orders: Vec::new(),
- });
+ RadrootsOrderRevisionOutcome::Declined { .. } => {
+ let mut projection =
+ request_projection(order_id, request, RadrootsOrderStatus::Declined);
+ projection.lifecycle_terminal = true;
+ projection.pending_revision_event_id = Some(proposal.event_id.clone());
+ projection.last_event_id = Some(decision.event_id.clone());
+ projection
}
}
- normalized.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
- (normalized, issues)
}
-fn listing_order_ids(
- requests: &[RadrootsOrderRequestRecord],
- decisions: &[RadrootsOrderDecisionRecord],
- revision_proposals: &[RadrootsOrderRevisionProposalRecord],
- revision_decisions: &[RadrootsOrderRevisionDecisionRecord],
- fulfillments: &[RadrootsOrderFulfillmentRecord],
- cancellations: &[RadrootsOrderCancellationRecord],
- receipts: &[RadrootsOrderReceiptRecord],
-) -> Vec<RadrootsOrderId> {
- let mut order_ids = Vec::new();
- order_ids.extend(
- requests
- .iter()
- .map(|request| request.payload.order_id.clone()),
- );
- order_ids.extend(
- decisions
- .iter()
- .map(|decision| decision.payload.order_id.clone()),
- );
- order_ids.extend(
- revision_proposals
- .iter()
- .map(|proposal| proposal.payload.order_id.clone()),
- );
- order_ids.extend(
- revision_decisions
- .iter()
- .map(|decision| decision.payload.order_id.clone()),
- );
- order_ids.extend(
- fulfillments
- .iter()
- .map(|fulfillment| fulfillment.payload.order_id.clone()),
- );
- order_ids.extend(
- cancellations
- .iter()
- .map(|cancellation| cancellation.payload.order_id.clone()),
- );
- order_ids.extend(
- receipts
- .iter()
- .map(|receipt| receipt.payload.order_id.clone()),
- );
- sort_and_dedup_values(&mut order_ids);
- order_ids
-}
-
-fn add_accepted_inventory_reservations_from_economics(
- bins: &mut [RadrootsListingInventoryBinAccounting],
+fn request_projection(
order_id: &RadrootsOrderId,
- agreement_event_id: &RadrootsEventId,
- economics: &RadrootsOrderEconomics,
- issues: &mut Vec<RadrootsListingInventoryAccountingIssue>,
-) {
- for item in &economics.items {
- if let Some(bin) = bins.iter_mut().find(|bin| bin.bin_id == item.bin_id) {
- add_inventory_reservation_event(
- bin,
- order_id,
- agreement_event_id,
- u64::from(item.bin_count),
- issues,
- );
- } else {
- issues.push(
- RadrootsListingInventoryAccountingIssue::UnknownInventoryBin {
- bin_id: item.bin_id.clone(),
- event_ids: vec![agreement_event_id.clone()],
- },
- );
- }
+ request: &RadrootsOrderRequestRecord,
+ status: RadrootsOrderStatus,
+) -> RadrootsOrderProjection {
+ RadrootsOrderProjection {
+ order_id: order_id.clone(),
+ status,
+ request_event_id: Some(request.event_id.clone()),
+ decision_event_id: None,
+ cancellation_event_id: None,
+ lifecycle_terminal: false,
+ economics: Some(request.payload.economics.clone()),
+ agreement_event_id: None,
+ pending_revision_event_id: None,
+ listing_addr: Some(request.payload.listing_addr.clone()),
+ buyer_pubkey: Some(request.payload.buyer_pubkey.clone()),
+ seller_pubkey: Some(request.payload.seller_pubkey.clone()),
+ last_event_id: Some(request.event_id.clone()),
+ issues: Vec::new(),
}
}
-#[cfg(test)]
-fn add_inventory_reservation(
- bin: &mut RadrootsListingInventoryBinAccounting,
+fn invalid_projection(
order_id: &RadrootsOrderId,
- decision: &RadrootsOrderDecisionRecord,
- bin_count: u64,
- issues: &mut Vec<RadrootsListingInventoryAccountingIssue>,
-) {
- add_inventory_reservation_event(bin, order_id, &decision.event_id, bin_count, issues);
+ request: Option<&RadrootsOrderRequestRecord>,
+ mut issues: Vec<RadrootsOrderIssue>,
+) -> RadrootsOrderProjection {
+ issues.sort_by(order_issue_sort_key);
+ let last_event_id = projection_issue_event_ids(&issues).into_iter().last();
+ match request {
+ Some(request) => {
+ let mut projection =
+ request_projection(order_id, request, RadrootsOrderStatus::Invalid);
+ projection.lifecycle_terminal = true;
+ projection.last_event_id = last_event_id.or_else(|| Some(request.event_id.clone()));
+ projection.issues = issues;
+ projection
+ }
+ None => {
+ let mut projection = empty_projection(order_id, RadrootsOrderStatus::Invalid, true);
+ projection.last_event_id = last_event_id;
+ projection.issues = issues;
+ projection
+ }
+ }
}
-fn add_inventory_reservation_event(
- bin: &mut RadrootsListingInventoryBinAccounting,
+fn empty_projection(
order_id: &RadrootsOrderId,
- event_id: &RadrootsEventId,
- bin_count: u64,
- issues: &mut Vec<RadrootsListingInventoryAccountingIssue>,
-) {
- if let Some(next_count) = bin.accepted_reserved_count.checked_add(bin_count) {
- bin.accepted_reserved_count = next_count;
- bin.accepted_orders
- .push(RadrootsListingInventoryOrderReservation {
- order_id: order_id.clone(),
- decision_event_id: event_id.clone(),
- bin_count,
- });
- } else {
- issues.push(
- RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
- bin_id: bin.bin_id.clone(),
- event_ids: vec![event_id.clone()],
- },
- );
- }
-}
-
-fn finish_inventory_accounting_bins(
- bins: &mut [RadrootsListingInventoryBinAccounting],
- issues: &mut Vec<RadrootsListingInventoryAccountingIssue>,
-) {
- for bin in bins.iter_mut() {
- bin.accepted_orders.sort_by(|left, right| {
- left.order_id
- .cmp(&right.order_id)
- .then_with(|| left.decision_event_id.cmp(&right.decision_event_id))
- });
- bin.remaining_count = bin
- .available_count
- .saturating_sub(bin.accepted_reserved_count);
- bin.over_reserved = bin.accepted_reserved_count > bin.available_count;
- if bin.over_reserved {
- let mut event_ids = bin
- .accepted_orders
- .iter()
- .map(|reservation| reservation.decision_event_id.clone())
- .collect::<Vec<_>>();
- sort_and_dedup_values(&mut event_ids);
- issues.push(RadrootsListingInventoryAccountingIssue::OverReserved {
- bin_id: bin.bin_id.clone(),
- available_count: bin.available_count,
- reserved_count: bin.accepted_reserved_count,
- event_ids,
- });
- }
- }
- bins.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
-}
-
-fn projection_issue_event_ids(issues: &[RadrootsOrderIssue]) -> Vec<RadrootsEventId> {
- let mut event_ids = Vec::new();
- for issue in issues {
- match issue {
- RadrootsOrderIssue::MissingRequest => {}
- RadrootsOrderIssue::MultipleRequests { event_ids: ids }
- | RadrootsOrderIssue::ConflictingDecisions { event_ids: ids }
- | RadrootsOrderIssue::DuplicatePayments { event_ids: ids }
- | RadrootsOrderIssue::DuplicateSettlements { event_ids: ids }
- | RadrootsOrderIssue::ForkedLifecycle { event_ids: ids } => {
- event_ids.extend(ids.iter().cloned());
- }
- RadrootsOrderIssue::RequestPayloadInvalid { event_id }
- | RadrootsOrderIssue::RequestOrderIdMismatch { event_id }
- | RadrootsOrderIssue::RequestAuthorMismatch { event_id }
- | RadrootsOrderIssue::RequestListingAddressInvalid { event_id }
- | RadrootsOrderIssue::RequestSellerListingMismatch { event_id }
- | RadrootsOrderIssue::DecisionPayloadInvalid { event_id }
- | RadrootsOrderIssue::DecisionOrderIdMismatch { event_id }
- | RadrootsOrderIssue::DecisionAuthorMismatch { event_id }
- | RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::DecisionBuyerMismatch { event_id }
- | RadrootsOrderIssue::DecisionSellerMismatch { event_id }
- | RadrootsOrderIssue::DecisionListingAddressInvalid { event_id }
- | RadrootsOrderIssue::DecisionListingMismatch { event_id }
- | RadrootsOrderIssue::DecisionRootMismatch { event_id }
- | RadrootsOrderIssue::DecisionPreviousMismatch { event_id }
- | RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id }
- | RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id }
- | RadrootsOrderIssue::DecisionMissingReason { event_id }
- | RadrootsOrderIssue::RevisionProposalWithoutAcceptedDecision { event_id }
- | RadrootsOrderIssue::RevisionProposalPayloadInvalid { event_id }
- | RadrootsOrderIssue::RevisionProposalOrderIdMismatch { event_id }
- | RadrootsOrderIssue::RevisionProposalAuthorMismatch { event_id }
- | RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::RevisionProposalBuyerMismatch { event_id }
- | RadrootsOrderIssue::RevisionProposalSellerMismatch { event_id }
- | RadrootsOrderIssue::RevisionProposalListingAddressInvalid { event_id }
- | RadrootsOrderIssue::RevisionProposalListingMismatch { event_id }
- | RadrootsOrderIssue::RevisionProposalRootMismatch { event_id }
- | RadrootsOrderIssue::RevisionProposalPreviousMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id }
- | RadrootsOrderIssue::RevisionDecisionPayloadInvalid { event_id }
- | RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionBuyerMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionSellerMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { event_id }
- | RadrootsOrderIssue::RevisionDecisionListingMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionRootMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionPreviousMismatch { event_id }
- | RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision { event_id }
- | RadrootsOrderIssue::FulfillmentPayloadInvalid { event_id }
- | RadrootsOrderIssue::FulfillmentOrderIdMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentAuthorMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentBuyerMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentSellerMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentListingAddressInvalid { event_id }
- | RadrootsOrderIssue::FulfillmentListingMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentRootMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentPreviousMismatch { event_id }
- | RadrootsOrderIssue::FulfillmentStatusNotPublishable { event_id }
- | RadrootsOrderIssue::FulfillmentUnsupportedTransition { event_id }
- | RadrootsOrderIssue::CancellationWithoutCancellableOrder { event_id }
- | RadrootsOrderIssue::CancellationPayloadInvalid { event_id }
- | RadrootsOrderIssue::CancellationOrderIdMismatch { event_id }
- | RadrootsOrderIssue::CancellationAuthorMismatch { event_id }
- | RadrootsOrderIssue::CancellationCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::CancellationBuyerMismatch { event_id }
- | RadrootsOrderIssue::CancellationSellerMismatch { event_id }
- | RadrootsOrderIssue::CancellationListingAddressInvalid { event_id }
- | RadrootsOrderIssue::CancellationListingMismatch { event_id }
- | RadrootsOrderIssue::CancellationRootMismatch { event_id }
- | RadrootsOrderIssue::CancellationPreviousMismatch { event_id }
- | RadrootsOrderIssue::CancellationAfterFulfillment { event_id }
- | RadrootsOrderIssue::ReceiptWithoutEligibleFulfillment { event_id }
- | RadrootsOrderIssue::ReceiptPayloadInvalid { event_id }
- | RadrootsOrderIssue::ReceiptOrderIdMismatch { event_id }
- | RadrootsOrderIssue::ReceiptAuthorMismatch { event_id }
- | RadrootsOrderIssue::ReceiptCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::ReceiptBuyerMismatch { event_id }
- | RadrootsOrderIssue::ReceiptSellerMismatch { event_id }
- | RadrootsOrderIssue::ReceiptListingAddressInvalid { event_id }
- | RadrootsOrderIssue::ReceiptListingMismatch { event_id }
- | RadrootsOrderIssue::ReceiptRootMismatch { event_id }
- | RadrootsOrderIssue::ReceiptPreviousMismatch { event_id }
- | RadrootsOrderIssue::PaymentWithoutAcceptedAgreement { event_id }
- | RadrootsOrderIssue::PaymentPayloadInvalid { event_id }
- | RadrootsOrderIssue::PaymentOrderIdMismatch { event_id }
- | RadrootsOrderIssue::PaymentAuthorMismatch { event_id }
- | RadrootsOrderIssue::PaymentCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::PaymentBuyerMismatch { event_id }
- | RadrootsOrderIssue::PaymentSellerMismatch { event_id }
- | RadrootsOrderIssue::PaymentListingAddressInvalid { event_id }
- | RadrootsOrderIssue::PaymentListingMismatch { event_id }
- | RadrootsOrderIssue::PaymentRootMismatch { event_id }
- | RadrootsOrderIssue::PaymentPreviousMismatch { event_id }
- | RadrootsOrderIssue::PaymentAgreementMismatch { event_id }
- | RadrootsOrderIssue::PaymentQuoteMismatch { event_id }
- | RadrootsOrderIssue::PaymentQuoteVersionMismatch { event_id }
- | RadrootsOrderIssue::PaymentEconomicsDigestMismatch { event_id }
- | RadrootsOrderIssue::PaymentAmountMismatch { event_id }
- | RadrootsOrderIssue::PaymentCurrencyMismatch { event_id }
- | RadrootsOrderIssue::PaymentAfterCancellation { event_id }
- | RadrootsOrderIssue::RevisionAfterPayment { event_id }
- | RadrootsOrderIssue::SettlementWithoutValidPayment { event_id }
- | RadrootsOrderIssue::SettlementPayloadInvalid { event_id }
- | RadrootsOrderIssue::SettlementOrderIdMismatch { event_id }
- | RadrootsOrderIssue::SettlementAuthorMismatch { event_id }
- | RadrootsOrderIssue::SettlementCounterpartyMismatch { event_id }
- | RadrootsOrderIssue::SettlementBuyerMismatch { event_id }
- | RadrootsOrderIssue::SettlementSellerMismatch { event_id }
- | RadrootsOrderIssue::SettlementListingAddressInvalid { event_id }
- | RadrootsOrderIssue::SettlementListingMismatch { event_id }
- | RadrootsOrderIssue::SettlementRootMismatch { event_id }
- | RadrootsOrderIssue::SettlementPreviousMismatch { event_id }
- | RadrootsOrderIssue::SettlementPaymentEventMismatch { event_id }
- | RadrootsOrderIssue::SettlementAgreementMismatch { event_id }
- | RadrootsOrderIssue::SettlementQuoteMismatch { event_id }
- | RadrootsOrderIssue::SettlementQuoteVersionMismatch { event_id }
- | RadrootsOrderIssue::SettlementEconomicsDigestMismatch { event_id }
- | RadrootsOrderIssue::SettlementAmountMismatch { event_id }
- | RadrootsOrderIssue::SettlementCurrencyMismatch { event_id } => {
- event_ids.push(event_id.clone());
- }
- RadrootsOrderIssue::ForkedFulfillments { event_ids: ids } => {
- event_ids.extend(ids.iter().cloned());
- }
- }
- }
- sort_and_dedup_values(&mut event_ids);
- event_ids
-}
-
-fn sort_and_dedup_values<T: Ord>(values: &mut Vec<T>) {
- values.sort();
- values.dedup();
-}
-
-fn inventory_issue_sort_key(
- left: &RadrootsListingInventoryAccountingIssue,
- right: &RadrootsListingInventoryAccountingIssue,
-) -> core::cmp::Ordering {
- inventory_issue_rank(left)
- .cmp(&inventory_issue_rank(right))
- .then_with(|| inventory_issue_id(left).cmp(inventory_issue_id(right)))
- .then_with(|| inventory_issue_event_ids(left).cmp(inventory_issue_event_ids(right)))
-}
-
-fn inventory_issue_rank(issue: &RadrootsListingInventoryAccountingIssue) -> u8 {
- match issue {
- RadrootsListingInventoryAccountingIssue::InvalidOrder { .. } => 0,
- RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { .. } => 1,
- RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { .. } => 2,
- RadrootsListingInventoryAccountingIssue::OverReserved { .. } => 3,
- }
-}
-
-fn inventory_issue_id(issue: &RadrootsListingInventoryAccountingIssue) -> &str {
- match issue {
- RadrootsListingInventoryAccountingIssue::InvalidOrder { order_id, .. } => order_id,
- RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { bin_id, .. }
- | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { bin_id, .. }
- | RadrootsListingInventoryAccountingIssue::OverReserved { bin_id, .. } => bin_id,
- }
-}
-
-fn inventory_issue_event_ids(
- issue: &RadrootsListingInventoryAccountingIssue,
-) -> &[RadrootsEventId] {
- match issue {
- RadrootsListingInventoryAccountingIssue::InvalidOrder { event_ids, .. }
- | RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { event_ids, .. }
- | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { event_ids, .. }
- | RadrootsListingInventoryAccountingIssue::OverReserved { event_ids, .. } => event_ids,
+ status: RadrootsOrderStatus,
+ lifecycle_terminal: bool,
+) -> RadrootsOrderProjection {
+ RadrootsOrderProjection {
+ order_id: order_id.clone(),
+ status,
+ request_event_id: None,
+ decision_event_id: None,
+ cancellation_event_id: None,
+ lifecycle_terminal,
+ economics: None,
+ agreement_event_id: None,
+ pending_revision_event_id: None,
+ listing_addr: None,
+ buyer_pubkey: None,
+ seller_pubkey: None,
+ last_event_id: None,
+ issues: Vec::new(),
}
}
@@ -2241,89 +1581,6 @@ fn validate_order_revision_decision_record(
valid
}
-fn validate_order_fulfillment_record(
- request: &RadrootsOrderRequestRecord,
- fulfillment: &RadrootsOrderFulfillmentRecord,
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> bool {
- let mut valid = true;
- if !fulfillment.payload.status.is_publishable_update() {
- issues.push(RadrootsOrderIssue::FulfillmentStatusNotPublishable {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- if fulfillment.payload.validate().is_err() {
- issues.push(RadrootsOrderIssue::FulfillmentPayloadInvalid {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- if fulfillment.payload.order_id != request.payload.order_id {
- issues.push(RadrootsOrderIssue::FulfillmentOrderIdMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- if fulfillment.author_pubkey != fulfillment.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::FulfillmentAuthorMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- if fulfillment.counterparty_pubkey != request.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::FulfillmentCounterpartyMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- if fulfillment.payload.buyer_pubkey != request.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::FulfillmentBuyerMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- if fulfillment.payload.seller_pubkey != request.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::FulfillmentSellerMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- match parse_public_listing_addr(&fulfillment.payload.listing_addr) {
- Ok(listing_addr) => {
- if fulfillment.payload.listing_addr != request.payload.listing_addr
- || listing_addr.seller_pubkey != fulfillment.payload.seller_pubkey
- {
- issues.push(RadrootsOrderIssue::FulfillmentListingMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- }
- Err(_) => {
- issues.push(RadrootsOrderIssue::FulfillmentListingAddressInvalid {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- }
- if fulfillment.root_event_id != request.event_id {
- issues.push(RadrootsOrderIssue::FulfillmentRootMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- if fulfillment.prev_event_id.trim().is_empty()
- || fulfillment.prev_event_id == fulfillment.event_id
- {
- issues.push(RadrootsOrderIssue::FulfillmentPreviousMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- valid = false;
- }
- valid
-}
-
fn validate_order_cancellation_record(
request: &RadrootsOrderRequestRecord,
cancellation: &RadrootsOrderCancellationRecord,
@@ -2401,4797 +1658,873 @@ fn validate_order_cancellation_record(
valid
}
-fn validate_order_receipt_record(
- request: &RadrootsOrderRequestRecord,
- receipt: &RadrootsOrderReceiptRecord,
+fn decision_payload_issue(
+ decision: &RadrootsOrderDecisionOutcome,
+ event_id: &RadrootsEventId,
issues: &mut Vec<RadrootsOrderIssue>,
) -> bool {
- let mut valid = true;
- if receipt.payload.validate().is_err() {
- issues.push(RadrootsOrderIssue::ReceiptPayloadInvalid {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- if receipt.payload.order_id != request.payload.order_id {
- issues.push(RadrootsOrderIssue::ReceiptOrderIdMismatch {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- if receipt.author_pubkey != receipt.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::ReceiptAuthorMismatch {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- if receipt.counterparty_pubkey != request.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::ReceiptCounterpartyMismatch {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- if receipt.payload.buyer_pubkey != request.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::ReceiptBuyerMismatch {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- if receipt.payload.seller_pubkey != request.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::ReceiptSellerMismatch {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- match parse_public_listing_addr(&receipt.payload.listing_addr) {
- Ok(listing_addr) => {
- if receipt.payload.listing_addr != request.payload.listing_addr
- || listing_addr.seller_pubkey != receipt.payload.seller_pubkey
- {
- issues.push(RadrootsOrderIssue::ReceiptListingMismatch {
- event_id: receipt.event_id.clone(),
+ match decision {
+ RadrootsOrderDecisionOutcome::Accepted {
+ inventory_commitments,
+ } => {
+ if inventory_commitments.is_empty() {
+ issues.push(RadrootsOrderIssue::DecisionMissingInventoryCommitments {
+ event_id: event_id.clone(),
});
- valid = false;
+ true
+ } else {
+ false
}
}
- Err(_) => {
- issues.push(RadrootsOrderIssue::ReceiptListingAddressInvalid {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
+ RadrootsOrderDecisionOutcome::Declined { reason } => {
+ if reason.trim().is_empty() {
+ issues.push(RadrootsOrderIssue::DecisionMissingReason {
+ event_id: event_id.clone(),
+ });
+ true
+ } else {
+ false
+ }
}
}
- if receipt.root_event_id != request.event_id {
- issues.push(RadrootsOrderIssue::ReceiptRootMismatch {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- if receipt.prev_event_id.trim().is_empty() || receipt.prev_event_id == receipt.event_id {
- issues.push(RadrootsOrderIssue::ReceiptPreviousMismatch {
- event_id: receipt.event_id.clone(),
- });
- valid = false;
- }
- valid
}
-fn reduce_order_payment_settlement_records(
- request: &RadrootsOrderRequestRecord,
- agreement_event_id: &RadrootsEventId,
- economics: &RadrootsOrderEconomics,
- payments: Vec<RadrootsOrderPaymentEventRecord>,
- settlements: Vec<RadrootsOrderSettlementRecord>,
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> RadrootsOrderPaymentProjection {
- let mut valid_payments = Vec::new();
- for payment in payments {
- if validate_order_payment_record(request, &payment, issues) {
- valid_payments.push(payment);
- }
- }
- let mut valid_settlements = Vec::new();
- for settlement in settlements {
- if validate_order_settlement_record(request, &settlement, issues) {
- valid_settlements.push(settlement);
- }
- }
- if !issues.is_empty() {
- return RadrootsOrderPaymentProjection::invalid();
- }
- if valid_payments.is_empty() {
- record_settlement_without_valid_payment(&valid_settlements, issues);
- return if issues.is_empty() {
- RadrootsOrderPaymentProjection::not_recorded()
- } else {
- RadrootsOrderPaymentProjection::invalid()
- };
- }
-
- let mut previous_payment_parent = agreement_event_id.clone();
- let mut used_payment_event_ids = Vec::new();
- let mut used_settlement_event_ids = Vec::new();
- let mut rejected_projection = None;
+fn unique_request_records(
+ requests: Vec<RadrootsOrderRequestRecord>,
+) -> Vec<RadrootsOrderRequestRecord> {
+ unique_records_by_event_id(requests, |record| &record.event_id)
+}
- loop {
- let payment_candidates = valid_payments
- .iter()
- .filter(|payment| {
- payment.prev_event_id == previous_payment_parent
- && payment.payload.previous_event_id == previous_payment_parent
- && !used_payment_event_ids.contains(&payment.event_id)
- })
- .collect::<Vec<_>>();
- if payment_candidates.is_empty() {
- for payment in valid_payments
- .iter()
- .filter(|payment| !used_payment_event_ids.contains(&payment.event_id))
- {
- issues.push(RadrootsOrderIssue::PaymentPreviousMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- for settlement in valid_settlements
- .iter()
- .filter(|settlement| !used_settlement_event_ids.contains(&settlement.event_id))
- {
- issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment {
- event_id: settlement.event_id.clone(),
- });
- }
- return if issues.is_empty() {
- rejected_projection.unwrap_or_else(RadrootsOrderPaymentProjection::not_recorded)
- } else {
- RadrootsOrderPaymentProjection::invalid()
- };
- }
- if payment_candidates.len() > 1 {
- let mut event_ids = payment_candidates
- .iter()
- .map(|payment| payment.event_id.clone())
- .collect::<Vec<_>>();
- event_ids.sort();
- issues.push(RadrootsOrderIssue::DuplicatePayments { event_ids });
- return RadrootsOrderPaymentProjection::invalid();
- }
- let payment = payment_candidates[0];
- validate_order_payment_agreement_record(payment, agreement_event_id, economics, issues);
- if !issues.is_empty() {
- return RadrootsOrderPaymentProjection::invalid();
- }
- used_payment_event_ids.push(payment.event_id.clone());
+fn unique_decision_records(
+ decisions: Vec<RadrootsOrderDecisionRecord>,
+) -> Vec<RadrootsOrderDecisionRecord> {
+ unique_records_by_event_id(decisions, |record| &record.event_id)
+}
- let settlement_candidates = valid_settlements
- .iter()
- .filter(|settlement| {
- settlement.prev_event_id == payment.event_id
- && settlement.payload.previous_event_id == payment.event_id
- && settlement.payload.payment_event_id == payment.event_id
- && !used_settlement_event_ids.contains(&settlement.event_id)
- })
- .collect::<Vec<_>>();
- if settlement_candidates.is_empty() {
- for settlement in valid_settlements
- .iter()
- .filter(|settlement| !used_settlement_event_ids.contains(&settlement.event_id))
- {
- issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment {
- event_id: settlement.event_id.clone(),
- });
- }
- return if issues.is_empty() {
- payment_projection_from_record(
- payment,
- RadrootsOrderPaymentState::Recorded,
- RadrootsOrderSettlementState::Pending,
- None,
- )
- } else {
- RadrootsOrderPaymentProjection::invalid()
- };
- }
- if settlement_candidates.len() > 1 {
- let mut event_ids = settlement_candidates
- .iter()
- .map(|settlement| settlement.event_id.clone())
- .collect::<Vec<_>>();
- event_ids.sort();
- issues.push(RadrootsOrderIssue::DuplicateSettlements { event_ids });
- return RadrootsOrderPaymentProjection::invalid();
- }
- let settlement = settlement_candidates[0];
- validate_order_settlement_payment_record(settlement, payment, issues);
- if !issues.is_empty() {
- return RadrootsOrderPaymentProjection::invalid();
- }
- used_settlement_event_ids.push(settlement.event_id.clone());
- match settlement.payload.decision {
- RadrootsOrderSettlementOutcome::Accepted => {
- for payment in valid_payments
- .iter()
- .filter(|payment| !used_payment_event_ids.contains(&payment.event_id))
- {
- issues.push(RadrootsOrderIssue::PaymentPreviousMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- for settlement in valid_settlements
- .iter()
- .filter(|settlement| !used_settlement_event_ids.contains(&settlement.event_id))
- {
- issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment {
- event_id: settlement.event_id.clone(),
- });
- }
- return if issues.is_empty() {
- payment_projection_from_record(
- payment,
- RadrootsOrderPaymentState::Settled,
- RadrootsOrderSettlementState::Accepted,
- Some(settlement),
- )
- } else {
- RadrootsOrderPaymentProjection::invalid()
- };
- }
- RadrootsOrderSettlementOutcome::Rejected => {
- rejected_projection = Some(payment_projection_from_record(
- payment,
- RadrootsOrderPaymentState::Rejected,
- RadrootsOrderSettlementState::Rejected,
- Some(settlement),
- ));
- previous_payment_parent = settlement.event_id.clone();
- }
- }
- }
+fn unique_revision_proposal_records(
+ revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>,
+) -> Vec<RadrootsOrderRevisionProposalRecord> {
+ unique_records_by_event_id(revision_proposals, |record| &record.event_id)
}
-fn payment_projection_from_record(
- payment: &RadrootsOrderPaymentEventRecord,
- state: RadrootsOrderPaymentState,
- settlement_state: RadrootsOrderSettlementState,
- settlement: Option<&RadrootsOrderSettlementRecord>,
-) -> RadrootsOrderPaymentProjection {
- RadrootsOrderPaymentProjection {
- state,
- settlement_state,
- payment_event_id: Some(payment.event_id.clone()),
- settlement_event_id: settlement.map(|settlement| settlement.event_id.clone()),
- agreement_event_id: Some(payment.payload.agreement_event_id.clone()),
- quote_id: Some(payment.payload.quote_id.clone()),
- quote_version: Some(payment.payload.quote_version),
- economics_digest: Some(payment.payload.economics_digest.clone()),
- amount: Some(payment.payload.amount),
- currency: Some(payment.payload.currency),
- method: Some(payment.payload.method),
- reference: payment.payload.reference.clone(),
- paid_at: payment.payload.paid_at,
- reason: settlement.and_then(|settlement| settlement.payload.reason.clone()),
- }
+fn unique_revision_decision_records(
+ revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>,
+) -> Vec<RadrootsOrderRevisionDecisionRecord> {
+ unique_records_by_event_id(revision_decisions, |record| &record.event_id)
}
-fn validate_order_payment_record(
- request: &RadrootsOrderRequestRecord,
- payment: &RadrootsOrderPaymentEventRecord,
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> bool {
- let mut valid = true;
- if payment.payload.validate().is_err() {
- issues.push(RadrootsOrderIssue::PaymentPayloadInvalid {
- event_id: payment.event_id.clone(),
- });
- valid = false;
- }
- if payment.payload.order_id != request.payload.order_id {
- issues.push(RadrootsOrderIssue::PaymentOrderIdMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
- }
- if payment.author_pubkey != payment.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::PaymentAuthorMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
- }
- if payment.counterparty_pubkey != request.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::PaymentCounterpartyMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
- }
- if payment.payload.buyer_pubkey != request.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::PaymentBuyerMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
- }
- if payment.payload.seller_pubkey != request.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::PaymentSellerMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
+fn unique_cancellation_records(
+ cancellations: Vec<RadrootsOrderCancellationRecord>,
+) -> Vec<RadrootsOrderCancellationRecord> {
+ unique_records_by_event_id(cancellations, |record| &record.event_id)
+}
+
+fn unique_records_by_event_id<T>(
+ mut records: Vec<T>,
+ event_id: impl Fn(&T) -> &RadrootsEventId,
+) -> Vec<T> {
+ let mut unique = Vec::new();
+ records.sort_by(|left, right| event_id(left).cmp(event_id(right)));
+ for record in records {
+ if unique
+ .iter()
+ .all(|existing: &T| event_id(existing) != event_id(&record))
+ {
+ unique.push(record);
+ }
}
- match parse_public_listing_addr(&payment.payload.listing_addr) {
- Ok(listing_addr) => {
- if payment.payload.listing_addr != request.payload.listing_addr
- || listing_addr.seller_pubkey != payment.payload.seller_pubkey
- {
- issues.push(RadrootsOrderIssue::PaymentListingMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
+ unique
+}
+
+fn normalized_listing_inventory_bins<I>(
+ bins: I,
+) -> (
+ Vec<RadrootsListingInventoryBinAccounting>,
+ Vec<RadrootsListingInventoryAccountingIssue>,
+)
+where
+ I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>,
+{
+ let mut normalized: Vec<RadrootsListingInventoryBinAccounting> = Vec::new();
+ let mut issues = Vec::new();
+ for bin in bins {
+ let bin_id = bin.bin_id;
+ if let Some(existing) = normalized
+ .iter_mut()
+ .find(|existing| existing.bin_id == bin_id)
+ {
+ if let Some(next_count) = existing.available_count.checked_add(bin.available_count) {
+ existing.available_count = next_count;
+ existing.remaining_count = next_count;
+ } else {
+ existing.available_count = u64::MAX;
+ existing.remaining_count = u64::MAX;
+ issues.push(
+ RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
+ bin_id: existing.bin_id.clone(),
+ event_ids: Vec::new(),
+ },
+ );
}
- }
- Err(_) => {
- issues.push(RadrootsOrderIssue::PaymentListingAddressInvalid {
- event_id: payment.event_id.clone(),
+ } else {
+ normalized.push(RadrootsListingInventoryBinAccounting {
+ bin_id,
+ available_count: bin.available_count,
+ accepted_reserved_count: 0,
+ remaining_count: bin.available_count,
+ over_reserved: false,
+ accepted_orders: Vec::new(),
});
- valid = false;
}
}
- if payment.root_event_id != request.event_id
- || payment.payload.root_event_id != request.event_id
- {
- issues.push(RadrootsOrderIssue::PaymentRootMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
- }
- if payment.prev_event_id.trim().is_empty()
- || payment.prev_event_id == payment.event_id
- || payment.payload.previous_event_id != payment.prev_event_id
- {
- issues.push(RadrootsOrderIssue::PaymentPreviousMismatch {
- event_id: payment.event_id.clone(),
- });
- valid = false;
- }
- valid
+ normalized.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
+ (normalized, issues)
+}
+
+fn listing_order_ids(
+ requests: &[RadrootsOrderRequestRecord],
+ decisions: &[RadrootsOrderDecisionRecord],
+ revision_proposals: &[RadrootsOrderRevisionProposalRecord],
+ revision_decisions: &[RadrootsOrderRevisionDecisionRecord],
+ cancellations: &[RadrootsOrderCancellationRecord],
+) -> Vec<RadrootsOrderId> {
+ let mut order_ids = Vec::new();
+ order_ids.extend(
+ requests
+ .iter()
+ .map(|request| request.payload.order_id.clone()),
+ );
+ order_ids.extend(
+ decisions
+ .iter()
+ .map(|decision| decision.payload.order_id.clone()),
+ );
+ order_ids.extend(
+ revision_proposals
+ .iter()
+ .map(|proposal| proposal.payload.order_id.clone()),
+ );
+ order_ids.extend(
+ revision_decisions
+ .iter()
+ .map(|decision| decision.payload.order_id.clone()),
+ );
+ order_ids.extend(
+ cancellations
+ .iter()
+ .map(|cancellation| cancellation.payload.order_id.clone()),
+ );
+ sort_and_dedup_values(&mut order_ids);
+ order_ids
}
-fn validate_order_payment_agreement_record(
- payment: &RadrootsOrderPaymentEventRecord,
+fn add_accepted_inventory_reservations_from_economics(
+ bins: &mut [RadrootsListingInventoryBinAccounting],
+ order_id: &RadrootsOrderId,
agreement_event_id: &RadrootsEventId,
economics: &RadrootsOrderEconomics,
- issues: &mut Vec<RadrootsOrderIssue>,
+ issues: &mut Vec<RadrootsListingInventoryAccountingIssue>,
) {
- if payment.payload.agreement_event_id.as_str() != agreement_event_id.as_str() {
- issues.push(RadrootsOrderIssue::PaymentAgreementMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- if payment.payload.quote_id != economics.quote_id {
- issues.push(RadrootsOrderIssue::PaymentQuoteMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- if payment.payload.quote_version != economics.quote_version {
- issues.push(RadrootsOrderIssue::PaymentQuoteVersionMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- if payment.payload.amount != economics.total.amount {
- issues.push(RadrootsOrderIssue::PaymentAmountMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- if payment.payload.currency != economics.total.currency
- || payment.payload.currency != economics.currency
- {
- issues.push(RadrootsOrderIssue::PaymentCurrencyMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- #[cfg(feature = "serde_json")]
- match radroots_order_economics_digest(economics) {
- Ok(expected_digest) if payment.payload.economics_digest != expected_digest => {
- issues.push(RadrootsOrderIssue::PaymentEconomicsDigestMismatch {
- event_id: payment.event_id.clone(),
- });
- }
- Ok(_) => {}
- Err(_) => {
- issues.push(RadrootsOrderIssue::PaymentEconomicsDigestMismatch {
- event_id: payment.event_id.clone(),
- });
+ for item in &economics.items {
+ if let Some(bin) = bins.iter_mut().find(|bin| bin.bin_id == item.bin_id) {
+ add_inventory_reservation_event(
+ bin,
+ order_id,
+ agreement_event_id,
+ u64::from(item.bin_count),
+ issues,
+ );
+ } else {
+ issues.push(
+ RadrootsListingInventoryAccountingIssue::UnknownInventoryBin {
+ bin_id: item.bin_id.clone(),
+ event_ids: vec![agreement_event_id.clone()],
+ },
+ );
}
}
}
-fn validate_order_settlement_record(
- request: &RadrootsOrderRequestRecord,
- settlement: &RadrootsOrderSettlementRecord,
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> bool {
- let mut valid = true;
- if settlement.payload.validate().is_err() {
- issues.push(RadrootsOrderIssue::SettlementPayloadInvalid {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
+fn add_inventory_reservation_event(
+ bin: &mut RadrootsListingInventoryBinAccounting,
+ order_id: &RadrootsOrderId,
+ event_id: &RadrootsEventId,
+ bin_count: u64,
+ issues: &mut Vec<RadrootsListingInventoryAccountingIssue>,
+) {
+ if let Some(next_count) = bin.accepted_reserved_count.checked_add(bin_count) {
+ bin.accepted_reserved_count = next_count;
+ bin.accepted_orders
+ .push(RadrootsListingInventoryOrderReservation {
+ order_id: order_id.clone(),
+ agreement_event_id: event_id.clone(),
+ bin_count,
+ });
+ } else {
+ issues.push(
+ RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
+ bin_id: bin.bin_id.clone(),
+ event_ids: vec![event_id.clone()],
+ },
+ );
}
- if settlement.payload.order_id != request.payload.order_id {
- issues.push(RadrootsOrderIssue::SettlementOrderIdMismatch {
- event_id: settlement.event_id.clone(),
+}
+
+fn finish_inventory_accounting_bins(
+ bins: &mut [RadrootsListingInventoryBinAccounting],
+ issues: &mut Vec<RadrootsListingInventoryAccountingIssue>,
+) {
+ for bin in bins.iter_mut() {
+ bin.accepted_orders.sort_by(|left, right| {
+ left.order_id
+ .cmp(&right.order_id)
+ .then_with(|| left.agreement_event_id.cmp(&right.agreement_event_id))
});
- valid = false;
- }
- if settlement.author_pubkey != settlement.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::SettlementAuthorMismatch {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
- }
- if settlement.counterparty_pubkey != request.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::SettlementCounterpartyMismatch {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
- }
- if settlement.payload.buyer_pubkey != request.payload.buyer_pubkey {
- issues.push(RadrootsOrderIssue::SettlementBuyerMismatch {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
- }
- if settlement.payload.seller_pubkey != request.payload.seller_pubkey {
- issues.push(RadrootsOrderIssue::SettlementSellerMismatch {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
- }
- match parse_public_listing_addr(&settlement.payload.listing_addr) {
- Ok(listing_addr) => {
- if settlement.payload.listing_addr != request.payload.listing_addr
- || listing_addr.seller_pubkey != settlement.payload.seller_pubkey
- {
- issues.push(RadrootsOrderIssue::SettlementListingMismatch {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
- }
- }
- Err(_) => {
- issues.push(RadrootsOrderIssue::SettlementListingAddressInvalid {
- event_id: settlement.event_id.clone(),
+ bin.remaining_count = bin
+ .available_count
+ .saturating_sub(bin.accepted_reserved_count);
+ bin.over_reserved = bin.accepted_reserved_count > bin.available_count;
+ if bin.over_reserved {
+ let mut event_ids = bin
+ .accepted_orders
+ .iter()
+ .map(|reservation| reservation.agreement_event_id.clone())
+ .collect::<Vec<_>>();
+ sort_and_dedup_values(&mut event_ids);
+ issues.push(RadrootsListingInventoryAccountingIssue::OverReserved {
+ bin_id: bin.bin_id.clone(),
+ available_count: bin.available_count,
+ reserved_count: bin.accepted_reserved_count,
+ event_ids,
});
- valid = false;
}
}
- if settlement.root_event_id != request.event_id
- || settlement.payload.root_event_id != request.event_id
- {
- issues.push(RadrootsOrderIssue::SettlementRootMismatch {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
- }
- if settlement.prev_event_id.trim().is_empty()
- || settlement.prev_event_id == settlement.event_id
- || settlement.payload.previous_event_id != settlement.prev_event_id
- {
- issues.push(RadrootsOrderIssue::SettlementPreviousMismatch {
- event_id: settlement.event_id.clone(),
- });
- valid = false;
- }
- valid
+ bins.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
}
-fn validate_order_settlement_payment_record(
- settlement: &RadrootsOrderSettlementRecord,
- payment: &RadrootsOrderPaymentEventRecord,
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- if settlement.payload.payment_event_id != payment.event_id {
- issues.push(RadrootsOrderIssue::SettlementPaymentEventMismatch {
- event_id: settlement.event_id.clone(),
- });
- }
- if settlement.payload.agreement_event_id != payment.payload.agreement_event_id {
- issues.push(RadrootsOrderIssue::SettlementAgreementMismatch {
- event_id: settlement.event_id.clone(),
- });
- }
- if settlement.payload.quote_id != payment.payload.quote_id {
- issues.push(RadrootsOrderIssue::SettlementQuoteMismatch {
- event_id: settlement.event_id.clone(),
- });
- }
- if settlement.payload.quote_version != payment.payload.quote_version {
- issues.push(RadrootsOrderIssue::SettlementQuoteVersionMismatch {
- event_id: settlement.event_id.clone(),
- });
- }
- if settlement.payload.economics_digest != payment.payload.economics_digest {
- issues.push(RadrootsOrderIssue::SettlementEconomicsDigestMismatch {
- event_id: settlement.event_id.clone(),
- });
+fn projection_issue_event_ids(issues: &[RadrootsOrderIssue]) -> Vec<RadrootsEventId> {
+ let mut event_ids = Vec::new();
+ for issue in issues {
+ match issue {
+ RadrootsOrderIssue::MissingRequest => {}
+ RadrootsOrderIssue::MultipleRequests { event_ids: ids }
+ | RadrootsOrderIssue::ConflictingDecisions { event_ids: ids }
+ | RadrootsOrderIssue::ForkedLifecycle { event_ids: ids } => {
+ event_ids.extend(ids.iter().cloned());
+ }
+ RadrootsOrderIssue::RequestPayloadInvalid { event_id }
+ | RadrootsOrderIssue::RequestOrderIdMismatch { event_id }
+ | RadrootsOrderIssue::RequestAuthorMismatch { event_id }
+ | RadrootsOrderIssue::RequestListingAddressInvalid { event_id }
+ | RadrootsOrderIssue::RequestSellerListingMismatch { event_id }
+ | RadrootsOrderIssue::DecisionPayloadInvalid { event_id }
+ | RadrootsOrderIssue::DecisionOrderIdMismatch { event_id }
+ | RadrootsOrderIssue::DecisionAuthorMismatch { event_id }
+ | RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id }
+ | RadrootsOrderIssue::DecisionBuyerMismatch { event_id }
+ | RadrootsOrderIssue::DecisionSellerMismatch { event_id }
+ | RadrootsOrderIssue::DecisionListingAddressInvalid { event_id }
+ | RadrootsOrderIssue::DecisionListingMismatch { event_id }
+ | RadrootsOrderIssue::DecisionRootMismatch { event_id }
+ | RadrootsOrderIssue::DecisionPreviousMismatch { event_id }
+ | RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id }
+ | RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id }
+ | RadrootsOrderIssue::DecisionMissingReason { event_id }
+ | RadrootsOrderIssue::RevisionProposalPayloadInvalid { event_id }
+ | RadrootsOrderIssue::RevisionProposalOrderIdMismatch { event_id }
+ | RadrootsOrderIssue::RevisionProposalAuthorMismatch { event_id }
+ | RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { event_id }
+ | RadrootsOrderIssue::RevisionProposalBuyerMismatch { event_id }
+ | RadrootsOrderIssue::RevisionProposalSellerMismatch { event_id }
+ | RadrootsOrderIssue::RevisionProposalListingAddressInvalid { event_id }
+ | RadrootsOrderIssue::RevisionProposalListingMismatch { event_id }
+ | RadrootsOrderIssue::RevisionProposalRootMismatch { event_id }
+ | RadrootsOrderIssue::RevisionProposalPreviousMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id }
+ | RadrootsOrderIssue::RevisionDecisionPayloadInvalid { event_id }
+ | RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionBuyerMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionSellerMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { event_id }
+ | RadrootsOrderIssue::RevisionDecisionListingMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionRootMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionPreviousMismatch { event_id }
+ | RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { event_id }
+ | RadrootsOrderIssue::CancellationWithoutCancellableOrder { event_id }
+ | RadrootsOrderIssue::CancellationPayloadInvalid { event_id }
+ | RadrootsOrderIssue::CancellationOrderIdMismatch { event_id }
+ | RadrootsOrderIssue::CancellationAuthorMismatch { event_id }
+ | RadrootsOrderIssue::CancellationCounterpartyMismatch { event_id }
+ | RadrootsOrderIssue::CancellationBuyerMismatch { event_id }
+ | RadrootsOrderIssue::CancellationSellerMismatch { event_id }
+ | RadrootsOrderIssue::CancellationListingAddressInvalid { event_id }
+ | RadrootsOrderIssue::CancellationListingMismatch { event_id }
+ | RadrootsOrderIssue::CancellationRootMismatch { event_id }
+ | RadrootsOrderIssue::CancellationPreviousMismatch { event_id } => {
+ event_ids.push(event_id.clone());
+ }
+ }
}
- if settlement.payload.amount != payment.payload.amount {
- issues.push(RadrootsOrderIssue::SettlementAmountMismatch {
- event_id: settlement.event_id.clone(),
- });
+ sort_and_dedup_values(&mut event_ids);
+ event_ids
+}
+
+fn parse_public_listing_addr(
+ value: impl AsRef<str>,
+) -> Result<RadrootsPublicListingAddress, RadrootsOrderCanonicalizationError> {
+ parse_public_listing_address(value).map_err(|error| match error {
+ RadrootsPublicListingAddressError::InvalidAddress(error) => {
+ RadrootsOrderCanonicalizationError::InvalidListingAddress(error.to_string())
+ }
+ RadrootsPublicListingAddressError::InvalidListingKind { .. } => {
+ RadrootsOrderCanonicalizationError::InvalidListingKind
+ }
+ RadrootsPublicListingAddressError::InvalidKind { .. } => {
+ RadrootsOrderCanonicalizationError::InvalidListingKind
+ }
+ })
+}
+
+fn canonicalize_items(
+ items: &mut [RadrootsOrderItem],
+) -> Result<(), RadrootsOrderCanonicalizationError> {
+ if items.is_empty() {
+ return Err(RadrootsOrderCanonicalizationError::MissingItems);
}
- if settlement.payload.currency != payment.payload.currency {
- issues.push(RadrootsOrderIssue::SettlementCurrencyMismatch {
- event_id: settlement.event_id.clone(),
- });
+ for (index, item) in items.iter().enumerate() {
+ if item.bin_count == 0 {
+ return Err(RadrootsOrderCanonicalizationError::InvalidBinCount { index });
+ }
}
+ items.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
+ Ok(())
}
-fn decision_payload_issue(
- decision: &RadrootsOrderDecisionOutcome,
- event_id: &RadrootsEventId,
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> bool {
+fn canonicalize_decision(
+ decision: &mut RadrootsOrderDecisionOutcome,
+) -> Result<(), RadrootsOrderCanonicalizationError> {
match decision {
RadrootsOrderDecisionOutcome::Accepted {
inventory_commitments,
} => {
if inventory_commitments.is_empty() {
- issues.push(RadrootsOrderIssue::DecisionMissingInventoryCommitments {
- event_id: event_id.clone(),
- });
- true
- } else {
- false
+ return Err(RadrootsOrderCanonicalizationError::MissingInventoryCommitments);
+ }
+ for (index, commitment) in inventory_commitments.iter().enumerate() {
+ if commitment.bin_count == 0 {
+ return Err(
+ RadrootsOrderCanonicalizationError::InvalidInventoryCommitmentCount {
+ index,
+ },
+ );
+ }
}
+ inventory_commitments.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
+ Ok(())
}
RadrootsOrderDecisionOutcome::Declined { reason } => {
if reason.trim().is_empty() {
- issues.push(RadrootsOrderIssue::DecisionMissingReason {
- event_id: event_id.clone(),
- });
- true
- } else {
- false
+ return Err(RadrootsOrderCanonicalizationError::EmptyField("reason"));
}
+ *reason = reason.trim().to_string();
+ Ok(())
}
}
}
-fn record_fulfillment_without_accepted_decision(
- fulfillments: &[RadrootsOrderFulfillmentRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for fulfillment in fulfillments {
- issues.push(RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision {
- event_id: fulfillment.event_id.clone(),
- });
- }
-}
-
-fn record_revision_proposal_without_accepted_decision(
- revision_proposals: &[RadrootsOrderRevisionProposalRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for proposal in revision_proposals {
- issues.push(
- RadrootsOrderIssue::RevisionProposalWithoutAcceptedDecision {
- event_id: proposal.event_id.clone(),
- },
- );
- }
-}
-
-fn record_revision_decision_without_proposal(
- revision_decisions: &[RadrootsOrderRevisionDecisionRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for decision in revision_decisions {
- issues.push(RadrootsOrderIssue::RevisionDecisionWithoutProposal {
- event_id: decision.event_id.clone(),
- });
- }
-}
-
-fn record_cancellation_without_cancellable_order(
- cancellations: &[RadrootsOrderCancellationRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for cancellation in cancellations {
- issues.push(RadrootsOrderIssue::CancellationWithoutCancellableOrder {
- event_id: cancellation.event_id.clone(),
- });
- }
-}
-
-fn record_receipt_without_eligible_fulfillment(
- receipts: &[RadrootsOrderReceiptRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for receipt in receipts {
- issues.push(RadrootsOrderIssue::ReceiptWithoutEligibleFulfillment {
- event_id: receipt.event_id.clone(),
- });
- }
-}
-
-fn record_payment_without_accepted_agreement(
- payments: &[RadrootsOrderPaymentEventRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for payment in payments {
- issues.push(RadrootsOrderIssue::PaymentWithoutAcceptedAgreement {
- event_id: payment.event_id.clone(),
- });
- }
-}
-
-fn record_settlement_without_valid_payment(
- settlements: &[RadrootsOrderSettlementRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for settlement in settlements {
- issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment {
- event_id: settlement.event_id.clone(),
- });
- }
-}
-
-fn record_payment_after_cancellation(
- payments: &[RadrootsOrderPaymentEventRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) {
- for payment in payments {
- issues.push(RadrootsOrderIssue::PaymentAfterCancellation {
- event_id: payment.event_id.clone(),
- });
- }
-}
-
-fn single_lifecycle_child<T>(
- records: &[T],
- event_id: impl Fn(&T) -> &RadrootsEventId,
-) -> Result<Option<T>, RadrootsOrderIssue>
-where
- T: Clone,
-{
- match records {
- [] => Ok(None),
- [record] => Ok(Some(record.clone())),
- _ => {
- let mut event_ids = records.iter().map(event_id).cloned().collect::<Vec<_>>();
- sort_and_dedup_values(&mut event_ids);
- Err(RadrootsOrderIssue::ForkedLifecycle { event_ids })
- }
- }
-}
-
-fn validated_fulfillment_records(
- request: &RadrootsOrderRequestRecord,
- fulfillments: Vec<RadrootsOrderFulfillmentRecord>,
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> Vec<RadrootsOrderFulfillmentRecord> {
- let mut valid_fulfillments = Vec::new();
- for fulfillment in fulfillments {
- if validate_order_fulfillment_record(request, &fulfillment, issues) {
- valid_fulfillments.push(fulfillment);
- }
- }
- valid_fulfillments
+fn inventory_commitments_match_request(
+ items: &[RadrootsOrderItem],
+ commitments: &[RadrootsOrderInventoryCommitment],
+) -> bool {
+ if items.len() != commitments.len() {
+ return false;
+ }
+ let mut expected = items.to_vec();
+ expected.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
+ let mut actual = commitments.to_vec();
+ actual.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
+ expected
+ .iter()
+ .zip(actual.iter())
+ .all(|(item, commitment)| {
+ item.bin_id == commitment.bin_id && item.bin_count == commitment.bin_count
+ })
}
-struct RadrootsOrderRevisionState {
- agreement_event_id: RadrootsEventId,
- lifecycle_parent_event_id: RadrootsEventId,
- economics: RadrootsOrderEconomics,
- pending_revision_event_id: Option<RadrootsEventId>,
+fn sort_and_dedup_values<T: Ord>(values: &mut Vec<T>) {
+ values.sort();
+ values.dedup();
}
-fn order_revision_state(
- request: &RadrootsOrderRequestRecord,
- decision: &RadrootsOrderDecisionRecord,
- revision_proposals: &[RadrootsOrderRevisionProposalRecord],
- revision_decisions: &[RadrootsOrderRevisionDecisionRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> Option<RadrootsOrderRevisionState> {
- let mut state = RadrootsOrderRevisionState {
- agreement_event_id: decision.event_id.clone(),
- lifecycle_parent_event_id: decision.event_id.clone(),
- economics: request.payload.economics.clone(),
- pending_revision_event_id: None,
- };
- let mut used_proposal_event_ids = Vec::new();
- let mut used_decision_event_ids = Vec::new();
-
- loop {
- let matching_proposals = revision_proposals
- .iter()
- .filter(|proposal| {
- proposal.prev_event_id == state.lifecycle_parent_event_id
- && !used_proposal_event_ids.contains(&proposal.event_id)
- })
- .cloned()
- .collect::<Vec<_>>();
- let proposal = match single_lifecycle_child(&matching_proposals, |record| &record.event_id)
- {
- Ok(Some(proposal)) => proposal,
- Ok(None) => break,
- Err(issue) => {
- issues.push(issue);
- return None;
- }
- };
- used_proposal_event_ids.push(proposal.event_id.clone());
- let matching_decisions = revision_decisions
- .iter()
- .filter(|decision| {
- decision.prev_event_id == proposal.event_id
- && !used_decision_event_ids.contains(&decision.event_id)
- })
- .cloned()
- .collect::<Vec<_>>();
- let revision_decision =
- match single_lifecycle_child(&matching_decisions, |record| &record.event_id) {
- Ok(Some(decision)) => decision,
- Ok(None) => {
- state.pending_revision_event_id = Some(proposal.event_id.clone());
- state.lifecycle_parent_event_id = proposal.event_id;
- break;
- }
- Err(issue) => {
- issues.push(issue);
- return None;
- }
- };
- if revision_decision.payload.revision_id != proposal.payload.revision_id {
- issues.push(RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch {
- event_id: revision_decision.event_id.clone(),
- });
- return None;
- }
- used_decision_event_ids.push(revision_decision.event_id.clone());
- match revision_decision.payload.decision {
- RadrootsOrderRevisionOutcome::Accepted => {
- state.agreement_event_id = revision_decision.event_id.clone();
- state.economics = proposal.payload.economics;
- }
- RadrootsOrderRevisionOutcome::Declined { .. } => {}
- }
- state.lifecycle_parent_event_id = revision_decision.event_id;
- }
-
- for proposal in revision_proposals {
- if !used_proposal_event_ids.contains(&proposal.event_id) {
- issues.push(RadrootsOrderIssue::RevisionProposalPreviousMismatch {
- event_id: proposal.event_id.clone(),
- });
- }
- }
- for decision in revision_decisions {
- if used_decision_event_ids.contains(&decision.event_id) {
- continue;
- }
- if let Some(proposal) = revision_proposals
- .iter()
- .find(|proposal| proposal.event_id == decision.prev_event_id)
- {
- if proposal.payload.revision_id != decision.payload.revision_id {
- issues.push(RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch {
- event_id: decision.event_id.clone(),
- });
- } else {
- issues.push(RadrootsOrderIssue::RevisionDecisionPreviousMismatch {
- event_id: decision.event_id.clone(),
- });
- }
- } else {
- issues.push(RadrootsOrderIssue::RevisionDecisionWithoutProposal {
- event_id: decision.event_id.clone(),
- });
- }
- }
-
- if issues.is_empty() { Some(state) } else { None }
-}
-
-fn latest_fulfillment_record(
- parent_event_id: &RadrootsEventId,
- valid_fulfillments: &[RadrootsOrderFulfillmentRecord],
- issues: &mut Vec<RadrootsOrderIssue>,
-) -> Option<RadrootsOrderFulfillmentRecord> {
- if !issues.is_empty() {
- return None;
- }
- let mut used_event_ids = Vec::new();
- let mut previous_event_id = parent_event_id.clone();
- let mut previous_status = RadrootsOrderFulfillmentState::AcceptedNotFulfilled;
- let mut latest = None;
-
- loop {
- let mut children = valid_fulfillments
- .iter()
- .filter(|fulfillment| {
- fulfillment.prev_event_id == previous_event_id
- && !used_event_ids.contains(&fulfillment.event_id)
- })
- .collect::<Vec<_>>();
- if children.is_empty() {
- break;
- }
- children.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- if children.len() > 1 {
- let mut event_ids = children
- .iter()
- .map(|fulfillment| fulfillment.event_id.clone())
- .collect::<Vec<_>>();
- event_ids.sort();
- issues.push(RadrootsOrderIssue::ForkedFulfillments { event_ids });
- return None;
- }
- let child = children[0];
- if matches!(
- previous_status,
- RadrootsOrderFulfillmentState::Delivered
- | RadrootsOrderFulfillmentState::SellerCancelled
- ) {
- issues.push(RadrootsOrderIssue::FulfillmentUnsupportedTransition {
- event_id: child.event_id.clone(),
- });
- return None;
- }
- used_event_ids.push(child.event_id.clone());
- previous_event_id = child.event_id.clone();
- previous_status = child.payload.status;
- latest = Some((*child).clone());
- }
-
- for fulfillment in valid_fulfillments {
- if !used_event_ids.contains(&fulfillment.event_id) {
- issues.push(RadrootsOrderIssue::FulfillmentPreviousMismatch {
- event_id: fulfillment.event_id.clone(),
- });
- }
- }
- latest
-}
-
-fn requested_projection(
- order_id: &RadrootsOrderId,
- request: &RadrootsOrderRequestRecord,
-) -> RadrootsOrderProjection {
- RadrootsOrderProjection {
- order_id: order_id.clone(),
- status: RadrootsOrderStatus::Requested,
- request_event_id: Some(request.event_id.clone()),
- decision_event_id: None,
- fulfillment_event_id: None,
- fulfillment_status: None,
- cancellation_event_id: None,
- receipt_event_id: None,
- receipt_received: None,
- receipt_issue: None,
- receipt_received_at: None,
- lifecycle_terminal: false,
- payment: RadrootsOrderPaymentProjection::not_recorded(),
- economics: Some(request.payload.economics.clone()),
- agreement_event_id: None,
- pending_revision_event_id: None,
- listing_addr: Some(request.payload.listing_addr.clone()),
- buyer_pubkey: Some(request.payload.buyer_pubkey.clone()),
- seller_pubkey: Some(request.payload.seller_pubkey.clone()),
- last_event_id: Some(request.event_id.clone()),
- issues: Vec::new(),
- }
-}
-
-fn requested_cancellation_projection(
- order_id: &RadrootsOrderId,
- request: &RadrootsOrderRequestRecord,
- cancellations: Vec<RadrootsOrderCancellationRecord>,
-) -> RadrootsOrderProjection {
- let mut issues = Vec::new();
- for cancellation in cancellations
- .iter()
- .filter(|cancellation| cancellation.prev_event_id != request.event_id)
- {
- issues.push(RadrootsOrderIssue::CancellationPreviousMismatch {
- event_id: cancellation.event_id.clone(),
- });
- }
- if !issues.is_empty() {
- return invalid_projection(order_id, Some(request), issues);
- }
- let matching = cancellations
- .into_iter()
- .filter(|cancellation| cancellation.prev_event_id == request.event_id)
- .collect::<Vec<_>>();
- match single_lifecycle_child(&matching, |record| &record.event_id) {
- Ok(Some(cancellation)) => cancelled_projection(
- order_id,
- request,
- None,
- None,
- request.payload.economics.clone(),
- cancellation,
- ),
- Ok(None) => requested_projection(order_id, request),
- Err(issue) => invalid_projection(order_id, Some(request), vec![issue]),
- }
-}
-
-fn decided_projection(
- order_id: &RadrootsOrderId,
- request: &RadrootsOrderRequestRecord,
- decision: &RadrootsOrderDecisionRecord,
- records: RadrootsOrderDecisionProjectionRecords,
-) -> RadrootsOrderProjection {
- let RadrootsOrderDecisionProjectionRecords {
- revision_proposals,
- revision_decisions,
- fulfillments,
- cancellations,
- receipts,
- payments,
- settlements,
- } = records;
- let status = match &decision.payload.decision {
- RadrootsOrderDecisionOutcome::Accepted { .. } => RadrootsOrderStatus::Accepted,
- RadrootsOrderDecisionOutcome::Declined { .. } => RadrootsOrderStatus::Declined,
- };
- let mut issues = Vec::new();
- let (
- fulfillment_event_id,
- fulfillment_status,
- last_event_id,
- agreement_event_id,
- pending_revision_event_id,
- economics,
- payment,
- ) = match status {
- RadrootsOrderStatus::Accepted => {
- let Some(revision_state) = order_revision_state(
- request,
- decision,
- &revision_proposals,
- &revision_decisions,
- &mut issues,
- ) else {
- return invalid_projection(order_id, Some(request), issues);
- };
- if let Some(pending_revision_event_id) =
- revision_state.pending_revision_event_id.as_ref()
- && (!fulfillments.is_empty()
- || !cancellations.is_empty()
- || !receipts.is_empty()
- || !payments.is_empty()
- || !settlements.is_empty())
- {
- let mut event_ids = vec![pending_revision_event_id.clone()];
- event_ids.extend(
- fulfillments
- .iter()
- .map(|fulfillment| fulfillment.event_id.clone()),
- );
- event_ids.extend(
- cancellations
- .iter()
- .map(|cancellation| cancellation.event_id.clone()),
- );
- event_ids.extend(receipts.iter().map(|receipt| receipt.event_id.clone()));
- event_ids.extend(payments.iter().map(|payment| payment.event_id.clone()));
- event_ids.extend(
- settlements
- .iter()
- .map(|settlement| settlement.event_id.clone()),
- );
- sort_and_dedup_values(&mut event_ids);
- return invalid_projection(
- order_id,
- Some(request),
- vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }],
- );
- }
- let fulfillment_records =
- validated_fulfillment_records(request, fulfillments, &mut issues);
- let latest = latest_fulfillment_record(
- &revision_state.lifecycle_parent_event_id,
- &fulfillment_records,
- &mut issues,
- );
- if !issues.is_empty() {
- return invalid_projection(order_id, Some(request), issues);
- }
- let decision_cancellations = cancellations
- .iter()
- .filter(|cancellation| {
- cancellation.prev_event_id == revision_state.lifecycle_parent_event_id
- })
- .cloned()
- .collect::<Vec<_>>();
- for cancellation in cancellations.iter().filter(|cancellation| {
- cancellation.prev_event_id != revision_state.lifecycle_parent_event_id
- }) {
- issues.push(RadrootsOrderIssue::CancellationPreviousMismatch {
- event_id: cancellation.event_id.clone(),
- });
- }
- if !issues.is_empty() {
- return invalid_projection(order_id, Some(request), issues);
- }
- if !decision_cancellations.is_empty() {
- record_payment_after_cancellation(&payments, &mut issues);
- record_settlement_without_valid_payment(&settlements, &mut issues);
- if !issues.is_empty() {
- return invalid_projection_with_payment(
- order_id,
- Some(request),
- issues,
- RadrootsOrderPaymentProjection::invalid(),
- );
- }
- }
- if let Some(first_fulfillment) = fulfillment_records.iter().find(|fulfillment| {
- fulfillment.prev_event_id == revision_state.lifecycle_parent_event_id
- }) && !decision_cancellations.is_empty()
- {
- let mut event_ids = decision_cancellations
- .iter()
- .map(|cancellation| cancellation.event_id.clone())
- .collect::<Vec<_>>();
- event_ids.push(first_fulfillment.event_id.clone());
- sort_and_dedup_values(&mut event_ids);
- return invalid_projection(
- order_id,
- Some(request),
- vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }],
- );
- }
- if latest.is_some() {
- for cancellation in decision_cancellations {
- issues.push(RadrootsOrderIssue::CancellationAfterFulfillment {
- event_id: cancellation.event_id,
- });
- }
- if !issues.is_empty() {
- return invalid_projection(order_id, Some(request), issues);
- }
- } else {
- match single_lifecycle_child(&decision_cancellations, |record| &record.event_id) {
- Ok(Some(cancellation)) => {
- return cancelled_projection(
- order_id,
- request,
- Some(decision.event_id.clone()),
- Some(revision_state.agreement_event_id.clone()),
- revision_state.economics.clone(),
- cancellation,
- );
- }
- Ok(None) => {}
- Err(issue) => {
- return invalid_projection(order_id, Some(request), vec![issue]);
- }
- }
- }
- let payment = reduce_order_payment_settlement_records(
- request,
- &revision_state.agreement_event_id,
- &revision_state.economics,
- payments,
- settlements,
- &mut issues,
- );
- if !issues.is_empty() {
- return invalid_projection_with_payment(
- order_id,
- Some(request),
- issues,
- RadrootsOrderPaymentProjection::invalid(),
- );
- }
- let receipt_result = receipt_projection(RadrootsReceiptProjectionInput {
- order_id,
- request,
- decision,
- agreement_event_id: &revision_state.agreement_event_id,
- economics: &revision_state.economics,
- latest_fulfillment: latest.as_ref(),
- fulfillments: &fulfillment_records,
- receipts,
- issues: &mut issues,
- });
- if let Some(mut projection) = receipt_result {
- projection.payment = payment;
- return projection;
- }
- if !issues.is_empty() {
- return invalid_projection(order_id, Some(request), issues);
- }
- let (fulfillment_event_id, fulfillment_status, last_event_id) = match latest {
- Some(fulfillment) => (
- Some(fulfillment.event_id.clone()),
- Some(fulfillment.payload.status),
- Some(fulfillment.event_id),
- ),
- None => (
- None,
- Some(RadrootsOrderFulfillmentState::AcceptedNotFulfilled),
- Some(revision_state.lifecycle_parent_event_id.clone()),
- ),
- };
- let mut projection_payment = payment;
- if projection_payment.state == RadrootsOrderPaymentState::NotRecorded {
- projection_payment.settlement_state = RadrootsOrderSettlementState::NotRequired;
- }
- (
- fulfillment_event_id,
- fulfillment_status,
- last_event_id,
- Some(revision_state.agreement_event_id),
- revision_state.pending_revision_event_id,
- Some(revision_state.economics),
- projection_payment,
- )
- }
- RadrootsOrderStatus::Declined => {
- record_revision_proposal_without_accepted_decision(&revision_proposals, &mut issues);
- record_revision_decision_without_proposal(&revision_decisions, &mut issues);
- record_payment_without_accepted_agreement(&payments, &mut issues);
- record_settlement_without_valid_payment(&settlements, &mut issues);
- if fulfillments.is_empty()
- && cancellations.is_empty()
- && receipts.is_empty()
- && payments.is_empty()
- && settlements.is_empty()
- && issues.is_empty()
- {
- (
- None,
- None,
- Some(decision.event_id.clone()),
- None,
- None,
- None,
- RadrootsOrderPaymentProjection::not_recorded(),
- )
- } else {
- record_fulfillment_without_accepted_decision(&fulfillments, &mut issues);
- record_cancellation_without_cancellable_order(&cancellations, &mut issues);
- record_receipt_without_eligible_fulfillment(&receipts, &mut issues);
- return invalid_projection_with_payment(
- order_id,
- Some(request),
- issues,
- RadrootsOrderPaymentProjection::invalid(),
- );
- }
- }
- _ => (
- None,
- None,
- Some(decision.event_id.clone()),
- None,
- None,
- None,
- RadrootsOrderPaymentProjection::not_recorded(),
- ),
- };
- RadrootsOrderProjection {
- order_id: order_id.clone(),
- status,
- request_event_id: Some(request.event_id.clone()),
- decision_event_id: Some(decision.event_id.clone()),
- fulfillment_event_id,
- fulfillment_status,
- cancellation_event_id: None,
- receipt_event_id: None,
- receipt_received: None,
- receipt_issue: None,
- receipt_received_at: None,
- lifecycle_terminal: false,
- payment,
- economics,
- agreement_event_id,
- pending_revision_event_id,
- listing_addr: Some(request.payload.listing_addr.clone()),
- buyer_pubkey: Some(request.payload.buyer_pubkey.clone()),
- seller_pubkey: Some(request.payload.seller_pubkey.clone()),
- last_event_id,
- issues: Vec::new(),
- }
-}
-
-fn receipt_projection(
- input: RadrootsReceiptProjectionInput<'_>,
-) -> Option<RadrootsOrderProjection> {
- let RadrootsReceiptProjectionInput {
- order_id,
- request,
- decision,
- agreement_event_id,
- economics,
- latest_fulfillment,
- fulfillments,
- receipts,
- issues,
- } = input;
- if receipts.is_empty() {
- return None;
- }
- let Some(fulfillment) = latest_fulfillment else {
- record_receipt_without_eligible_fulfillment(&receipts, issues);
- return None;
- };
- if !matches!(
- fulfillment.payload.status,
- RadrootsOrderFulfillmentState::ReadyForPickup | RadrootsOrderFulfillmentState::Delivered
- ) {
- record_receipt_without_eligible_fulfillment(&receipts, issues);
- return None;
- }
- let mut fork_event_ids = Vec::new();
- for receipt in &receipts {
- let Some(receipt_parent) = fulfillments
- .iter()
- .find(|candidate| candidate.event_id == receipt.prev_event_id)
- else {
- continue;
- };
- if !matches!(
- receipt_parent.payload.status,
- RadrootsOrderFulfillmentState::ReadyForPickup
- | RadrootsOrderFulfillmentState::Delivered
- ) {
- continue;
- }
- let sibling_fulfillment_event_ids = fulfillments
- .iter()
- .filter(|candidate| candidate.prev_event_id == receipt.prev_event_id)
- .map(|candidate| candidate.event_id.clone())
- .collect::<Vec<_>>();
- if !sibling_fulfillment_event_ids.is_empty() {
- fork_event_ids.push(receipt.event_id.clone());
- fork_event_ids.extend(sibling_fulfillment_event_ids);
- }
- }
- if !fork_event_ids.is_empty() {
- sort_and_dedup_values(&mut fork_event_ids);
- issues.push(RadrootsOrderIssue::ForkedLifecycle {
- event_ids: fork_event_ids,
- });
- return None;
- }
- let matching = receipts
- .iter()
- .filter(|receipt| receipt.prev_event_id == fulfillment.event_id)
- .cloned()
- .collect::<Vec<_>>();
- match single_lifecycle_child(&matching, |record| &record.event_id) {
- Ok(Some(receipt)) => Some(receipt_terminal_projection(
- order_id,
- request,
- decision,
- agreement_event_id,
- economics,
- fulfillment,
- receipt,
- )),
- Ok(None) => {
- for receipt in receipts {
- issues.push(RadrootsOrderIssue::ReceiptPreviousMismatch {
- event_id: receipt.event_id,
- });
- }
- None
- }
- Err(issue) => {
- issues.push(issue);
- None
- }
- }
-}
-
-fn cancelled_projection(
- order_id: &RadrootsOrderId,
- request: &RadrootsOrderRequestRecord,
- decision_event_id: Option<RadrootsEventId>,
- agreement_event_id: Option<RadrootsEventId>,
- economics: RadrootsOrderEconomics,
- cancellation: RadrootsOrderCancellationRecord,
-) -> RadrootsOrderProjection {
- RadrootsOrderProjection {
- order_id: order_id.clone(),
- status: RadrootsOrderStatus::Cancelled,
- request_event_id: Some(request.event_id.clone()),
- decision_event_id,
- fulfillment_event_id: None,
- fulfillment_status: None,
- cancellation_event_id: Some(cancellation.event_id.clone()),
- receipt_event_id: None,
- receipt_received: None,
- receipt_issue: None,
- receipt_received_at: None,
- lifecycle_terminal: true,
- payment: RadrootsOrderPaymentProjection::not_recorded(),
- economics: Some(economics),
- agreement_event_id,
- pending_revision_event_id: None,
- listing_addr: Some(request.payload.listing_addr.clone()),
- buyer_pubkey: Some(request.payload.buyer_pubkey.clone()),
- seller_pubkey: Some(request.payload.seller_pubkey.clone()),
- last_event_id: Some(cancellation.event_id),
- issues: Vec::new(),
- }
-}
-
-fn receipt_terminal_projection(
- order_id: &RadrootsOrderId,
- request: &RadrootsOrderRequestRecord,
- decision: &RadrootsOrderDecisionRecord,
- agreement_event_id: &RadrootsEventId,
- economics: &RadrootsOrderEconomics,
- fulfillment: &RadrootsOrderFulfillmentRecord,
- receipt: RadrootsOrderReceiptRecord,
-) -> RadrootsOrderProjection {
- let status = if receipt.payload.received {
- RadrootsOrderStatus::Completed
- } else {
- RadrootsOrderStatus::Disputed
- };
- RadrootsOrderProjection {
- order_id: order_id.clone(),
- status,
- request_event_id: Some(request.event_id.clone()),
- decision_event_id: Some(decision.event_id.clone()),
- fulfillment_event_id: Some(fulfillment.event_id.clone()),
- fulfillment_status: Some(fulfillment.payload.status),
- cancellation_event_id: None,
- receipt_event_id: Some(receipt.event_id.clone()),
- receipt_received: Some(receipt.payload.received),
- receipt_issue: receipt.payload.issue.clone(),
- receipt_received_at: Some(receipt.payload.received_at),
- lifecycle_terminal: true,
- payment: RadrootsOrderPaymentProjection::not_recorded(),
- economics: Some(economics.clone()),
- agreement_event_id: Some(agreement_event_id.clone()),
- pending_revision_event_id: None,
- listing_addr: Some(request.payload.listing_addr.clone()),
- buyer_pubkey: Some(request.payload.buyer_pubkey.clone()),
- seller_pubkey: Some(request.payload.seller_pubkey.clone()),
- last_event_id: Some(receipt.event_id),
- issues: Vec::new(),
- }
-}
-
-fn invalid_projection(
- order_id: &RadrootsOrderId,
- request: Option<&RadrootsOrderRequestRecord>,
- issues: Vec<RadrootsOrderIssue>,
-) -> RadrootsOrderProjection {
- invalid_projection_with_payment(
- order_id,
- request,
- issues,
- RadrootsOrderPaymentProjection::not_recorded(),
- )
-}
-
-fn invalid_projection_with_payment(
- order_id: &RadrootsOrderId,
- request: Option<&RadrootsOrderRequestRecord>,
- issues: Vec<RadrootsOrderIssue>,
- payment: RadrootsOrderPaymentProjection,
-) -> RadrootsOrderProjection {
- let economics = match request {
- Some(request) if request.payload.validate().is_ok() => {
- Some(request.payload.economics.clone())
- }
- _ => None,
- };
- RadrootsOrderProjection {
- order_id: order_id.clone(),
- status: RadrootsOrderStatus::Invalid,
- request_event_id: request.map(|request| request.event_id.clone()),
- decision_event_id: None,
- fulfillment_event_id: None,
- fulfillment_status: None,
- cancellation_event_id: None,
- receipt_event_id: None,
- receipt_received: None,
- receipt_issue: None,
- receipt_received_at: None,
- lifecycle_terminal: true,
- payment,
- economics,
- agreement_event_id: None,
- pending_revision_event_id: None,
- listing_addr: request.map(|request| request.payload.listing_addr.clone()),
- buyer_pubkey: request.map(|request| request.payload.buyer_pubkey.clone()),
- seller_pubkey: request.map(|request| request.payload.seller_pubkey.clone()),
- last_event_id: request.map(|request| request.event_id.clone()),
- issues,
- }
-}
-
-fn parse_public_listing_addr(
- listing_addr_raw: &str,
-) -> Result<RadrootsPublicListingAddress, RadrootsOrderCanonicalizationError> {
- parse_public_listing_address(listing_addr_raw).map_err(|error| match error {
- RadrootsPublicListingAddressError::InvalidAddress(error) => {
- RadrootsOrderCanonicalizationError::InvalidListingAddress(error.to_string())
- }
- RadrootsPublicListingAddressError::InvalidListingKind { .. }
- | RadrootsPublicListingAddressError::InvalidKind { .. } => {
- RadrootsOrderCanonicalizationError::InvalidListingKind
- }
- })
-}
-
-fn canonicalize_items(
- items: &mut Vec<RadrootsOrderItem>,
-) -> Result<(), RadrootsOrderCanonicalizationError> {
- if items.is_empty() {
- return Err(RadrootsOrderCanonicalizationError::MissingItems);
- }
- let mut canonical_items: Vec<RadrootsOrderItem> = Vec::new();
- for (index, item) in items.iter_mut().enumerate() {
- if item.bin_count == 0 {
- return Err(RadrootsOrderCanonicalizationError::InvalidBinCount { index });
- }
- if let Some(existing) = canonical_items
- .iter_mut()
- .find(|canonical| canonical.bin_id.as_str() == item.bin_id.as_str())
- {
- existing.bin_count = existing
- .bin_count
- .checked_add(item.bin_count)
- .ok_or(RadrootsOrderCanonicalizationError::InvalidBinCount { index })?;
- } else {
- canonical_items.push(RadrootsOrderItem {
- bin_id: item.bin_id.clone(),
- bin_count: item.bin_count,
- });
- }
- }
- canonical_items.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
- *items = canonical_items;
- Ok(())
-}
-
-fn canonicalize_decision(
- decision: &mut RadrootsOrderDecisionOutcome,
-) -> Result<(), RadrootsOrderCanonicalizationError> {
- match decision {
- RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments,
- } => canonicalize_inventory_commitments(inventory_commitments),
- RadrootsOrderDecisionOutcome::Declined { reason } => {
- *reason = normalized_required_string(core::mem::take(reason), "reason")?;
- Ok(())
- }
- }
-}
-
-fn canonicalize_inventory_commitments(
- commitments: &mut [RadrootsOrderInventoryCommitment],
-) -> Result<(), RadrootsOrderCanonicalizationError> {
- if commitments.is_empty() {
- return Err(RadrootsOrderCanonicalizationError::MissingInventoryCommitments);
- }
- for (index, commitment) in commitments.iter_mut().enumerate() {
- if commitment.bin_count == 0 {
- return Err(
- RadrootsOrderCanonicalizationError::InvalidInventoryCommitmentCount { index },
- );
- }
- }
- Ok(())
-}
-
-#[derive(Debug, PartialEq, Eq)]
-struct NormalizedInventoryCount {
- bin_id: RadrootsInventoryBinId,
- bin_count: u64,
-}
-
-fn inventory_commitments_match_request(
- request_items: &[RadrootsOrderItem],
- inventory_commitments: &[RadrootsOrderInventoryCommitment],
-) -> bool {
- normalized_request_item_counts(request_items)
- == normalized_inventory_commitment_counts(inventory_commitments)
-}
-
-fn normalized_request_item_counts(
- items: &[RadrootsOrderItem],
-) -> Option<Vec<NormalizedInventoryCount>> {
- let mut counts = Vec::new();
- for item in items {
- push_normalized_inventory_count(&mut counts, &item.bin_id, item.bin_count)?;
- }
- counts.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
- Some(counts)
-}
-
-fn normalized_inventory_commitment_counts(
- commitments: &[RadrootsOrderInventoryCommitment],
-) -> Option<Vec<NormalizedInventoryCount>> {
- let mut counts = Vec::new();
- for commitment in commitments {
- push_normalized_inventory_count(&mut counts, &commitment.bin_id, commitment.bin_count)?;
- }
- counts.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
- Some(counts)
-}
-
-fn push_normalized_inventory_count(
- counts: &mut Vec<NormalizedInventoryCount>,
- bin_id: &RadrootsInventoryBinId,
- bin_count: u32,
-) -> Option<()> {
- if bin_count == 0 {
- return None;
- }
- if let Some(existing) = counts
- .iter_mut()
- .find(|count| count.bin_id.as_str() == bin_id.as_str())
- {
- existing.bin_count = existing.bin_count.checked_add(u64::from(bin_count))?;
- } else {
- counts.push(NormalizedInventoryCount {
- bin_id: bin_id.clone(),
- bin_count: u64::from(bin_count),
- });
- }
- Some(())
-}
-
-fn normalized_required_string(
- value: String,
- field: &'static str,
-) -> Result<String, RadrootsOrderCanonicalizationError> {
- let value = value.trim().to_string();
- if value.is_empty() {
- return Err(RadrootsOrderCanonicalizationError::EmptyField(field));
- }
- Ok(value)
-}
-
-#[cfg(test)]
-mod tests {
- use core::fmt::Write as _;
- use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
- };
- #[cfg(feature = "event_store")]
- use radroots_event_store::{RadrootsEventIngest, RadrootsEventStore};
- use radroots_events::ids::{
- RadrootsEconomicsDigest, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress,
- RadrootsOrderId, RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey,
- };
- use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT};
- use radroots_events::order::{
- RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
- RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics,
- RadrootsOrderFulfillmentState, RadrootsOrderFulfillmentUpdate,
- RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPaymentMethod,
- RadrootsOrderPaymentRecord as RadrootsOrderPaymentPayload, RadrootsOrderPricingBasis,
- RadrootsOrderReceipt, RadrootsOrderRequest, RadrootsOrderRevisionDecision,
- RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal,
- RadrootsOrderSettlementDecision, RadrootsOrderSettlementOutcome,
- };
- use radroots_events::tags::{TAG_E_PREV, TAG_E_ROOT};
- use radroots_events::{RadrootsNostrEvent, RadrootsNostrEventPtr};
- use radroots_events_codec::order::{
- RadrootsOrderEnvelopeParseError, order_cancellation_event_build,
- order_decision_event_build, order_fulfillment_update_event_build,
- order_payment_record_event_build, order_receipt_event_build, order_request_event_build,
- order_revision_decision_event_build, order_revision_proposal_event_build,
- order_settlement_decision_event_build,
- };
- use radroots_events_codec::wire::WireEventParts;
- #[cfg(feature = "event_store")]
- use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp,
- radroots_event_from_nostr, radroots_nostr_build_event,
- };
-
- #[cfg(feature = "event_store")]
- use super::{
- ORDER_EVENT_CONTRACT_IDS, RadrootsOrderStoreQueryError, order_events_for_order_id,
- order_projection_for_order_id, order_projection_query_for_order_id,
- };
- use super::{
- RadrootsListingInventoryAccountingInputs, RadrootsListingInventoryAccountingIssue,
- RadrootsListingInventoryAccountingProjection, RadrootsListingInventoryBinAccounting,
- RadrootsListingInventoryBinAvailability, RadrootsListingInventoryOrderReservation,
- RadrootsOrderCancellationRecord, RadrootsOrderCanonicalizationError,
- RadrootsOrderDecisionRecord, RadrootsOrderEventDecodeError, RadrootsOrderEventRecord,
- RadrootsOrderFulfillmentRecord, RadrootsOrderIssue, RadrootsOrderPaymentEventRecord,
- RadrootsOrderPaymentProjection, RadrootsOrderPaymentState, RadrootsOrderProjection,
- RadrootsOrderReceiptRecord, RadrootsOrderReductionInputs, RadrootsOrderRequestRecord,
- RadrootsOrderRevisionDecisionRecord, RadrootsOrderRevisionProposalRecord,
- RadrootsOrderSettlementRecord, RadrootsOrderSettlementState, RadrootsOrderStatus,
- add_inventory_reservation, canonicalize_order_decision_for_signer,
- canonicalize_order_request_for_signer, inventory_issue_event_ids, inventory_issue_id,
- inventory_issue_rank, inventory_issue_sort_key, order_event_record_from_event,
- projection_issue_event_ids, radroots_order_economics_digest,
- reduce_listing_inventory_accounting as reduce_listing_inventory_accounting_with_revisions_inner,
- reduce_order_event_records,
- reduce_order_events as reduce_order_events_with_revisions_inner,
- };
-
- const SELLER: &str = "1111111111111111111111111111111111111111111111111111111111111111";
- const BUYER: &str = "2222222222222222222222222222222222222222222222222222222222222222";
- #[cfg(feature = "event_store")]
- const STORE_BUYER_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
- #[cfg(feature = "event_store")]
- const STORE_BUYER_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
- #[cfg(feature = "event_store")]
- const STORE_SELLER_SECRET_KEY_HEX: &str =
- "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8";
- #[cfg(feature = "event_store")]
- const STORE_SELLER_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
-
- fn order_id(raw: &str) -> RadrootsOrderId {
- RadrootsOrderId::parse(raw).expect("order id")
- }
-
- fn pubkey(raw: &str) -> RadrootsPublicKey {
- RadrootsPublicKey::parse(raw).expect("public key")
- }
-
- fn pubkey_or(default: &str, raw: &str) -> RadrootsPublicKey {
- if raw.is_empty() {
- pubkey(default)
- } else {
- pubkey(raw)
- }
- }
-
- fn test_event_id(raw: &str) -> RadrootsEventId {
- let mut bytes = [0u8; 32];
- for (index, byte) in raw.bytes().enumerate() {
- let primary = index % bytes.len();
- let secondary = (index * 7 + 13) % bytes.len();
- bytes[primary] = bytes[primary]
- .wrapping_add(byte)
- .wrapping_add((index as u8).wrapping_mul(31));
- bytes[secondary] ^= byte.rotate_left((index % 8) as u32);
- }
- let mut hex = String::with_capacity(64);
- for byte in bytes {
- write!(&mut hex, "{byte:02x}").unwrap();
- }
- RadrootsEventId::parse(hex).expect("event id")
- }
-
- fn order_revision_id(raw: &str) -> RadrootsOrderRevisionId {
- RadrootsOrderRevisionId::parse(raw).expect("revision id")
- }
-
- fn order_quote_id(raw: &str) -> RadrootsOrderQuoteId {
- RadrootsOrderQuoteId::parse(raw).expect("quote id")
- }
-
- fn bin_id(raw: &str) -> RadrootsInventoryBinId {
- RadrootsInventoryBinId::parse(raw).expect("bin id")
- }
-
- fn listing_address() -> RadrootsListingAddress {
- RadrootsListingAddress::parse(listing_addr()).expect("listing address")
- }
-
- fn economics_digest(raw: impl AsRef<str>) -> RadrootsEconomicsDigest {
- RadrootsEconomicsDigest::parse(raw.as_ref()).expect("economics digest")
- }
-
- fn sample_order_request(buyer_pubkey: &str, seller_pubkey: &str) -> RadrootsOrderRequest {
- RadrootsOrderRequest {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey_or(BUYER, buyer_pubkey),
- seller_pubkey: pubkey_or(SELLER, seller_pubkey),
- items: vec![RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }],
- economics: request_economics("bin-1", 2, "10"),
- }
- }
-
- fn decimal(raw: &str) -> RadrootsCoreDecimal {
- raw.parse().unwrap()
- }
-
- fn usd(raw: &str) -> RadrootsCoreMoney {
- RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD)
- }
-
- fn request_economics(
- raw_bin_id: &str,
- bin_count: u32,
- subtotal: &str,
- ) -> RadrootsOrderEconomics {
- RadrootsOrderEconomics {
- quote_id: order_quote_id("quote-1"),
- quote_version: 1,
- pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
- currency: RadrootsCoreCurrency::USD,
- items: vec![RadrootsOrderEconomicItem {
- bin_id: bin_id(raw_bin_id),
- bin_count,
- quantity_amount: decimal("1"),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: decimal("5"),
- unit_price_currency: RadrootsCoreCurrency::USD,
- line_subtotal: usd(subtotal),
- }],
- discounts: Vec::<RadrootsOrderEconomicLine>::new(),
- adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
- subtotal: usd(subtotal),
- discount_total: usd("0"),
- adjustment_total: usd("0"),
- total: usd(subtotal),
- }
- }
-
- fn sample_order_decision(seller_pubkey: &str) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey_or(SELLER, seller_pubkey),
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }],
- },
- }
- }
-
- fn listing_addr() -> String {
- format!("{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- }
-
- fn listing_event_ptr() -> RadrootsNostrEventPtr {
- RadrootsNostrEventPtr {
- id: test_event_id("listing-event").into_string(),
- relays: Some("wss://relay.radroots.test".to_string()),
- }
- }
-
- #[cfg(feature = "event_store")]
- fn store_fixture_keys(secret_key_hex: &str) -> RadrootsNostrKeys {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- RadrootsNostrKeys::new(secret_key)
- }
-
- #[cfg(feature = "event_store")]
- fn signed_store_event_from_parts(
- secret_key_hex: &str,
- created_at: u32,
- parts: WireEventParts,
- ) -> RadrootsNostrEvent {
- let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .expect("event builder")
- .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at)))
- .sign_with_keys(&store_fixture_keys(secret_key_hex))
- .expect("signed event");
- radroots_event_from_nostr(&event)
- }
-
- #[cfg(feature = "event_store")]
- fn signed_store_event(
- secret_key_hex: &str,
- kind: u32,
- created_at: u32,
- tags: Vec<Vec<String>>,
- content: impl Into<String>,
- ) -> RadrootsNostrEvent {
- let event = radroots_nostr_build_event(kind, content, tags)
- .expect("event builder")
- .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at)))
- .sign_with_keys(&store_fixture_keys(secret_key_hex))
- .expect("signed event");
- radroots_event_from_nostr(&event)
- }
-
- #[cfg(feature = "event_store")]
- fn tamper_store_event_signature(event: &mut RadrootsNostrEvent) {
- let replacement = if event.sig.starts_with('0') { "1" } else { "0" };
- event.sig.replace_range(0..1, replacement);
- }
-
- #[cfg(feature = "event_store")]
- fn store_listing_address() -> RadrootsListingAddress {
- RadrootsListingAddress::parse(format!(
- "{KIND_LISTING}:{STORE_SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("store listing address")
- }
-
- #[cfg(feature = "event_store")]
- fn store_listing_event_ptr() -> RadrootsNostrEventPtr {
- RadrootsNostrEventPtr {
- id: test_event_id("store-listing-event").into_string(),
- relays: Some("wss://relay.radroots.test".to_string()),
- }
- }
-
- #[cfg(feature = "event_store")]
- fn store_order_request(raw_order_id: &str) -> RadrootsOrderRequest {
- RadrootsOrderRequest {
- order_id: order_id(raw_order_id),
- listing_addr: store_listing_address(),
- buyer_pubkey: pubkey(STORE_BUYER_PUBLIC_KEY_HEX),
- seller_pubkey: pubkey(STORE_SELLER_PUBLIC_KEY_HEX),
- items: vec![RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }],
- economics: request_economics("bin-1", 2, "10"),
- }
- }
-
- #[cfg(feature = "event_store")]
- fn store_order_decision(raw_order_id: &str) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: order_id(raw_order_id),
- listing_addr: store_listing_address(),
- buyer_pubkey: pubkey(STORE_BUYER_PUBLIC_KEY_HEX),
- seller_pubkey: pubkey(STORE_SELLER_PUBLIC_KEY_HEX),
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }],
- },
- }
- }
-
- #[cfg(feature = "event_store")]
- fn store_order_request_event(raw_order_id: &str, created_at: u32) -> RadrootsNostrEvent {
- let request = store_order_request(raw_order_id);
- signed_store_event_from_parts(
- STORE_BUYER_SECRET_KEY_HEX,
- created_at,
- order_request_event_build(&store_listing_event_ptr(), &request).unwrap(),
- )
- }
-
- #[cfg(feature = "event_store")]
- fn store_order_decision_event(
- raw_order_id: &str,
- root_event_id: &RadrootsEventId,
- created_at: u32,
- ) -> RadrootsNostrEvent {
- let decision = store_order_decision(raw_order_id);
- signed_store_event_from_parts(
- STORE_SELLER_SECRET_KEY_HEX,
- created_at,
- order_decision_event_build(root_event_id, root_event_id, &decision).unwrap(),
- )
- }
-
- fn event_from_parts(
- event_id: &RadrootsEventId,
- author_pubkey: &RadrootsPublicKey,
- parts: WireEventParts,
- ) -> RadrootsNostrEvent {
- RadrootsNostrEvent {
- id: event_id.clone().into_string(),
- author: author_pubkey.clone().into_string(),
- created_at: 1,
- kind: parts.kind,
- tags: parts.tags,
- content: parts.content,
- sig: "sig".to_string(),
- }
- }
-
- fn clean_request_payload() -> RadrootsOrderRequest {
- RadrootsOrderRequest {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- items: vec![RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }],
- economics: request_economics("bin-1", 2, "10"),
- }
- }
-
- fn request_record_with_event_id(event_id: &str) -> RadrootsOrderRequestRecord {
- RadrootsOrderRequestRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(BUYER),
- payload: clean_request_payload(),
- }
- }
-
- fn request_record() -> RadrootsOrderRequestRecord {
- request_record_with_event_id("request-1")
- }
-
- fn request_record_for(
- raw_order_id: &str,
- event_id: &str,
- bin_count: u32,
- ) -> RadrootsOrderRequestRecord {
- let mut request = request_record_with_event_id(event_id);
- request.payload.order_id = order_id(raw_order_id);
- request.payload.items[0].bin_count = bin_count;
- let subtotal =
- (RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count)).to_string();
- request.payload.economics = request_economics("bin-1", bin_count, &subtotal);
- request
- }
-
- fn decision_payload(decision: RadrootsOrderDecisionOutcome) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- decision,
- }
- }
-
- fn accepted_decision_record(event_id: &str) -> RadrootsOrderDecisionRecord {
- RadrootsOrderDecisionRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(SELLER),
- counterparty_pubkey: pubkey(BUYER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id("request-1"),
- payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }],
- }),
- }
- }
-
- fn declined_decision_record(event_id: &str) -> RadrootsOrderDecisionRecord {
- RadrootsOrderDecisionRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(SELLER),
- counterparty_pubkey: pubkey(BUYER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id("request-1"),
- payload: decision_payload(RadrootsOrderDecisionOutcome::Declined {
- reason: "out_of_stock".to_string(),
- }),
- }
- }
-
- fn fulfillment_record(
- event_id: &str,
- prev_event_id: &str,
- status: RadrootsOrderFulfillmentState,
- ) -> RadrootsOrderFulfillmentRecord {
- RadrootsOrderFulfillmentRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(SELLER),
- counterparty_pubkey: pubkey(BUYER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- payload: RadrootsOrderFulfillmentUpdate {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- status,
- },
- }
- }
-
- fn cancellation_record(event_id: &str, prev_event_id: &str) -> RadrootsOrderCancellationRecord {
- RadrootsOrderCancellationRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(BUYER),
- counterparty_pubkey: pubkey(SELLER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- payload: RadrootsOrderCancellation {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- reason: "changed plans".to_string(),
- },
- }
- }
-
- fn receipt_record(
- event_id: &str,
- prev_event_id: &str,
- received: bool,
- ) -> RadrootsOrderReceiptRecord {
- RadrootsOrderReceiptRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(BUYER),
- counterparty_pubkey: pubkey(SELLER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- payload: RadrootsOrderReceipt {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- received,
- issue: (!received).then(|| "damaged items".to_string()),
- received_at: 1_777_665_600,
- },
- }
- }
-
- fn payment_record(event_id: &str, prev_event_id: &str) -> RadrootsOrderPaymentEventRecord {
- let economics = request_economics("bin-1", 2, "10");
- RadrootsOrderPaymentEventRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(BUYER),
- counterparty_pubkey: pubkey(SELLER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- payload: RadrootsOrderPaymentPayload {
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- root_event_id: test_event_id("request-1"),
- previous_event_id: test_event_id(prev_event_id),
- agreement_event_id: test_event_id("decision-1"),
- quote_id: economics.quote_id.clone(),
- quote_version: economics.quote_version,
- economics_digest: economics_digest(
- radroots_order_economics_digest(&economics).unwrap(),
- ),
- amount: economics.total.amount,
- currency: economics.total.currency,
- method: RadrootsOrderPaymentMethod::ManualTransfer,
- reference: Some("manual reference".to_string()),
- paid_at: Some(1_777_666_000),
- },
- }
- }
-
- fn settlement_record(
- event_id: &str,
- payment_event_id: &str,
- decision: RadrootsOrderSettlementOutcome,
- ) -> RadrootsOrderSettlementRecord {
- let payment = payment_record(payment_event_id, "decision-1");
- RadrootsOrderSettlementRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(SELLER),
- counterparty_pubkey: pubkey(BUYER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(payment_event_id),
- payload: RadrootsOrderSettlementDecision {
- order_id: payment.payload.order_id,
- listing_addr: payment.payload.listing_addr,
- seller_pubkey: payment.payload.seller_pubkey,
- buyer_pubkey: payment.payload.buyer_pubkey,
- root_event_id: payment.payload.root_event_id.clone(),
- previous_event_id: test_event_id(payment_event_id),
- agreement_event_id: payment.payload.agreement_event_id,
- payment_event_id: test_event_id(payment_event_id),
- quote_id: payment.payload.quote_id,
- quote_version: payment.payload.quote_version,
- economics_digest: payment.payload.economics_digest,
- amount: payment.payload.amount,
- currency: payment.payload.currency,
- decision,
- reason: (decision == RadrootsOrderSettlementOutcome::Rejected)
- .then(|| "reference mismatch".to_string()),
- },
- }
- }
-
- fn accepted_decision_record_for(
- raw_order_id: &str,
- event_id: &str,
- request_event_id: &str,
- bin_count: u32,
- ) -> RadrootsOrderDecisionRecord {
- let mut decision = accepted_decision_record(event_id);
- decision.root_event_id = test_event_id(request_event_id);
- decision.prev_event_id = test_event_id(request_event_id);
- decision.payload.order_id = order_id(raw_order_id);
- let RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments,
- } = &mut decision.payload.decision
- else {
- panic!("expected accepted decision")
- };
- inventory_commitments[0].bin_count = bin_count;
- decision
- }
-
- fn inventory_bin(available_count: u64) -> RadrootsListingInventoryBinAvailability {
- RadrootsListingInventoryBinAvailability {
- bin_id: bin_id("bin-1"),
- available_count,
- }
- }
-
- fn revision_proposal_record(
- event_id: &str,
- prev_event_id: &str,
- revision_id: &str,
- bin_count: u32,
- ) -> RadrootsOrderRevisionProposalRecord {
- let subtotal =
- (RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count)).to_string();
- RadrootsOrderRevisionProposalRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(SELLER),
- counterparty_pubkey: pubkey(BUYER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- payload: RadrootsOrderRevisionProposal {
- revision_id: order_revision_id(revision_id),
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- items: vec![RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count,
- }],
- economics: request_economics("bin-1", bin_count, &subtotal),
- reason: "field yield changed".to_string(),
- },
- }
- }
-
- fn revision_decision_record(
- event_id: &str,
- prev_event_id: &str,
- revision_id: &str,
- decision: RadrootsOrderRevisionOutcome,
- ) -> RadrootsOrderRevisionDecisionRecord {
- RadrootsOrderRevisionDecisionRecord {
- event_id: test_event_id(event_id),
- author_pubkey: pubkey(BUYER),
- counterparty_pubkey: pubkey(SELLER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- payload: RadrootsOrderRevisionDecision {
- revision_id: order_revision_id(revision_id),
- order_id: order_id("order-1"),
- listing_addr: listing_address(),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- root_event_id: test_event_id("request-1"),
- prev_event_id: test_event_id(prev_event_id),
- decision,
- },
- }
- }
-
- #[test]
- fn order_event_record_accessors_cover_all_variants() {
- let records = vec![
- (
- RadrootsOrderEventRecord::Request(request_record_with_event_id("record-request")),
- "record-request",
- ),
- (
- RadrootsOrderEventRecord::Decision(accepted_decision_record("record-decision")),
- "record-decision",
- ),
- (
- RadrootsOrderEventRecord::RevisionProposal(revision_proposal_record(
- "record-revision-proposal",
- "decision-1",
- "revision-1",
- 3,
- )),
- "record-revision-proposal",
- ),
- (
- RadrootsOrderEventRecord::RevisionDecision(revision_decision_record(
- "record-revision-decision",
- "record-revision-proposal",
- "revision-1",
- RadrootsOrderRevisionOutcome::Accepted,
- )),
- "record-revision-decision",
- ),
- (
- RadrootsOrderEventRecord::Fulfillment(fulfillment_record(
- "record-fulfillment",
- "decision-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- )),
- "record-fulfillment",
- ),
- (
- RadrootsOrderEventRecord::Cancellation(cancellation_record(
- "record-cancellation",
- "request-1",
- )),
- "record-cancellation",
- ),
- (
- RadrootsOrderEventRecord::Receipt(receipt_record(
- "record-receipt",
- "record-fulfillment",
- true,
- )),
- "record-receipt",
- ),
- (
- RadrootsOrderEventRecord::Payment(payment_record("record-payment", "decision-1")),
- "record-payment",
- ),
- (
- RadrootsOrderEventRecord::Settlement(settlement_record(
- "record-settlement",
- "record-payment",
- RadrootsOrderSettlementOutcome::Accepted,
- )),
- "record-settlement",
- ),
- ];
-
- for (record, event_id_raw) in records {
- let expected_event_id = test_event_id(event_id_raw);
- assert_eq!(record.event_id(), &expected_event_id);
- assert_eq!(record.order_id(), &order_id("order-1"));
- }
- }
-
- #[test]
- fn order_event_record_from_event_decodes_all_variants() {
- let request = request_record_with_event_id("decode-request");
- let request_event = event_from_parts(
- &request.event_id,
- &request.author_pubkey,
- order_request_event_build(&listing_event_ptr(), &request.payload).unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&request_event).unwrap(),
- RadrootsOrderEventRecord::Request(request)
- );
-
- let decision = accepted_decision_record("decode-decision");
- let decision_event = event_from_parts(
- &decision.event_id,
- &decision.author_pubkey,
- order_decision_event_build(
- &decision.root_event_id,
- &decision.prev_event_id,
- &decision.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&decision_event).unwrap(),
- RadrootsOrderEventRecord::Decision(decision)
- );
-
- let proposal = revision_proposal_record(
- "decode-revision-proposal",
- "decode-decision",
- "revision-1",
- 3,
- );
- let proposal_event = event_from_parts(
- &proposal.event_id,
- &proposal.author_pubkey,
- order_revision_proposal_event_build(
- &proposal.root_event_id,
- &proposal.prev_event_id,
- &proposal.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&proposal_event).unwrap(),
- RadrootsOrderEventRecord::RevisionProposal(proposal)
- );
-
- let revision_decision = revision_decision_record(
- "decode-revision-decision",
- "decode-revision-proposal",
- "revision-1",
- RadrootsOrderRevisionOutcome::Accepted,
- );
- let revision_decision_event = event_from_parts(
- &revision_decision.event_id,
- &revision_decision.author_pubkey,
- order_revision_decision_event_build(
- &revision_decision.root_event_id,
- &revision_decision.prev_event_id,
- &revision_decision.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&revision_decision_event).unwrap(),
- RadrootsOrderEventRecord::RevisionDecision(revision_decision)
- );
-
- let fulfillment = fulfillment_record(
- "decode-fulfillment",
- "decode-revision-decision",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- );
- let fulfillment_event = event_from_parts(
- &fulfillment.event_id,
- &fulfillment.author_pubkey,
- order_fulfillment_update_event_build(
- &fulfillment.root_event_id,
- &fulfillment.prev_event_id,
- &fulfillment.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&fulfillment_event).unwrap(),
- RadrootsOrderEventRecord::Fulfillment(fulfillment)
- );
-
- let cancellation = cancellation_record("decode-cancellation", "decode-request");
- let cancellation_event = event_from_parts(
- &cancellation.event_id,
- &cancellation.author_pubkey,
- order_cancellation_event_build(
- &cancellation.root_event_id,
- &cancellation.prev_event_id,
- &cancellation.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&cancellation_event).unwrap(),
- RadrootsOrderEventRecord::Cancellation(cancellation)
- );
-
- let receipt = receipt_record("decode-receipt", "decode-fulfillment", true);
- let receipt_event = event_from_parts(
- &receipt.event_id,
- &receipt.author_pubkey,
- order_receipt_event_build(
- &receipt.root_event_id,
- &receipt.prev_event_id,
- &receipt.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&receipt_event).unwrap(),
- RadrootsOrderEventRecord::Receipt(receipt)
- );
-
- let payment = payment_record("decode-payment", "decode-decision");
- let payment_event = event_from_parts(
- &payment.event_id,
- &payment.author_pubkey,
- order_payment_record_event_build(
- &payment.root_event_id,
- &payment.prev_event_id,
- &payment.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&payment_event).unwrap(),
- RadrootsOrderEventRecord::Payment(payment)
- );
-
- let settlement = settlement_record(
- "decode-settlement",
- "decode-payment",
- RadrootsOrderSettlementOutcome::Accepted,
- );
- let settlement_event = event_from_parts(
- &settlement.event_id,
- &settlement.author_pubkey,
- order_settlement_decision_event_build(
- &settlement.root_event_id,
- &settlement.prev_event_id,
- &settlement.payload,
- )
- .unwrap(),
- );
- assert_eq!(
- order_event_record_from_event(&settlement_event).unwrap(),
- RadrootsOrderEventRecord::Settlement(settlement)
- );
- }
-
- #[test]
- fn order_event_record_from_event_rejects_wrong_kind() {
- let request = request_record_with_event_id("decode-wrong-kind");
- let mut event = event_from_parts(
- &request.event_id,
- &request.author_pubkey,
- order_request_event_build(&listing_event_ptr(), &request.payload).unwrap(),
- );
- event.kind = KIND_LISTING;
- assert!(matches!(
- order_event_record_from_event(&event),
- Err(RadrootsOrderEventDecodeError::UnsupportedKind { kind: KIND_LISTING })
- ));
- }
-
- #[test]
- fn order_event_record_from_event_rejects_wrong_envelope_type() {
- let request = request_record_with_event_id("decode-request-content");
- let request_parts =
- order_request_event_build(&listing_event_ptr(), &request.payload).unwrap();
- let decision = accepted_decision_record("decode-wrong-envelope");
- let mut event = event_from_parts(
- &decision.event_id,
- &decision.author_pubkey,
- order_decision_event_build(
- &decision.root_event_id,
- &decision.prev_event_id,
- &decision.payload,
- )
- .unwrap(),
- );
- event.content = request_parts.content;
- assert!(matches!(
- order_event_record_from_event(&event),
- Err(RadrootsOrderEventDecodeError::Envelope(_))
- ));
- }
-
- #[test]
- fn order_event_record_from_event_rejects_root_and_previous_mismatches() {
- let proposal =
- revision_proposal_record("decode-chain-mismatch", "decode-decision", "revision-1", 3);
- let parts = order_revision_proposal_event_build(
- &proposal.root_event_id,
- &proposal.prev_event_id,
- &proposal.payload,
- )
- .unwrap();
- let mut root_event = event_from_parts(&proposal.event_id, &proposal.author_pubkey, parts);
- root_event
- .tags
- .iter_mut()
- .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT))
- .unwrap()[1] = test_event_id("wrong-root").into_string();
- assert!(matches!(
- order_event_record_from_event(&root_event),
- Err(RadrootsOrderEventDecodeError::Envelope(
- RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("root_event_id")
- ))
- ));
-
- let parts = order_revision_proposal_event_build(
- &proposal.root_event_id,
- &proposal.prev_event_id,
- &proposal.payload,
- )
- .unwrap();
- let mut prev_event = event_from_parts(&proposal.event_id, &proposal.author_pubkey, parts);
- prev_event
- .tags
- .iter_mut()
- .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_PREV))
- .unwrap()[1] = test_event_id("wrong-prev").into_string();
- assert!(matches!(
- order_event_record_from_event(&prev_event),
- Err(RadrootsOrderEventDecodeError::Envelope(
- RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("prev_event_id")
- ))
- ));
- }
-
- #[test]
- fn order_event_record_from_event_rejects_counterparty_mismatch() {
- let decision = accepted_decision_record("decode-counterparty-mismatch");
- let mut event = event_from_parts(
- &decision.event_id,
- &decision.author_pubkey,
- order_decision_event_build(
- &decision.root_event_id,
- &decision.prev_event_id,
- &decision.payload,
- )
- .unwrap(),
- );
- event
- .tags
- .iter_mut()
- .find(|tag| tag.first().map(String::as_str) == Some("p"))
- .unwrap()[1] = SELLER.to_string();
- assert!(matches!(
- order_event_record_from_event(&event),
- Err(RadrootsOrderEventDecodeError::Envelope(
- RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch
- ))
- ));
- }
-
- #[test]
- fn reduce_order_event_records_dedupes_duplicate_event_ids() {
- let request = request_record_with_event_id("unified-duplicate");
- let mut duplicate = request.clone();
- duplicate.payload.items[0].bin_count = 4;
-
- let projection = reduce_order_event_records(
- &order_id("order-1"),
- [
- RadrootsOrderEventRecord::Request(request.clone()),
- RadrootsOrderEventRecord::Request(duplicate),
- ],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Requested);
- assert_eq!(projection.request_event_id, Some(request.event_id));
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_event_records_is_stable_for_shuffled_input() {
- let request = request_record();
- let decision = accepted_decision_record("decision-1");
- let fulfillment = fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- );
- let receipt = receipt_record("receipt-1", "fulfillment-1", true);
-
- let grouped = reduce_order_events(
- "order-1",
- [request.clone()],
- [decision.clone()],
- [fulfillment.clone()],
- [],
- [receipt.clone()],
- );
- let unified = reduce_order_event_records(
- &order_id("order-1"),
- [
- RadrootsOrderEventRecord::Receipt(receipt),
- RadrootsOrderEventRecord::Fulfillment(fulfillment),
- RadrootsOrderEventRecord::Decision(decision),
- RadrootsOrderEventRecord::Request(request),
- ],
- );
-
- assert_eq!(unified, grouped);
- }
-
- #[test]
- fn reduce_order_event_records_reports_missing_for_empty_stream() {
- let projection = reduce_order_event_records(
- &order_id("order-1"),
- Vec::<RadrootsOrderEventRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Missing);
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_event_records_reports_multiple_requests_deterministically() {
- let first = request_record_with_event_id("request-a");
- let second = request_record_with_event_id("request-b");
- let projection = reduce_order_event_records(
- &order_id("order-1"),
- [
- RadrootsOrderEventRecord::Request(second.clone()),
- RadrootsOrderEventRecord::Request(first.clone()),
- ],
- );
- let reversed = reduce_order_event_records(
- &order_id("order-1"),
- [
- RadrootsOrderEventRecord::Request(first),
- RadrootsOrderEventRecord::Request(second),
- ],
- );
-
- assert_eq!(projection, reversed);
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(
- projection
- .issues
- .iter()
- .any(|issue| matches!(issue, RadrootsOrderIssue::MultipleRequests { .. }))
- );
- }
-
- #[cfg(feature = "event_store")]
- #[tokio::test]
- async fn order_events_for_order_id_queries_d_tag_and_filters_store_rows() {
- let store = RadrootsEventStore::open_memory().await.expect("store");
- let request_event = store_order_request_event("order-1", 10);
- let request_event_id = RadrootsEventId::parse(&request_event.id).expect("request id");
- let decision_event = store_order_decision_event("order-1", &request_event_id, 11);
- let wrong_order_event = store_order_request_event("order-2", 12);
- let wrong_contract_event = signed_store_event(
- STORE_BUYER_SECRET_KEY_HEX,
- KIND_LISTING,
- 13,
- vec![vec!["d".to_string(), "order-1".to_string()]],
- "{}",
- );
- let mut unprojected_order_event = store_order_request_event("order-1", 14);
- tamper_store_event_signature(&mut unprojected_order_event);
-
- for (event, observed_at_ms) in [
- (wrong_contract_event, 1_000),
- (request_event.clone(), 1_100),
- (wrong_order_event, 1_200),
- (unprojected_order_event, 1_300),
- (decision_event.clone(), 1_400),
- ] {
- store
- .ingest_event(RadrootsEventIngest::new(event, observed_at_ms))
- .await
- .expect("ingest");
- }
-
- let records = order_events_for_order_id(&store, &order_id("order-1"), 10)
- .await
- .expect("order events");
-
- assert_eq!(ORDER_EVENT_CONTRACT_IDS.len(), 9);
- assert_eq!(records.len(), 2);
- assert!(matches!(records[0], RadrootsOrderEventRecord::Request(_)));
- assert!(matches!(records[1], RadrootsOrderEventRecord::Decision(_)));
- assert_eq!(records[0].event_id().as_str(), request_event.id.as_str());
- assert_eq!(records[1].event_id().as_str(), decision_event.id.as_str());
- assert!(
- records
- .iter()
- .all(|record| record.order_id().as_str() == "order-1")
- );
- }
-
- #[cfg(feature = "event_store")]
- #[tokio::test]
- async fn order_projection_for_order_id_reduces_store_events() {
- let store = RadrootsEventStore::open_memory().await.expect("store");
- let request_event = store_order_request_event("order-1", 20);
- let request_event_id = RadrootsEventId::parse(&request_event.id).expect("request id");
- let decision_event = store_order_decision_event("order-1", &request_event_id, 21);
-
- for (event, observed_at_ms) in [(request_event, 2_000), (decision_event.clone(), 2_100)] {
- store
- .ingest_event(RadrootsEventIngest::new(event, observed_at_ms))
- .await
- .expect("ingest");
- }
-
- let projection = order_projection_for_order_id(&store, &order_id("order-1"), 10)
- .await
- .expect("projection");
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection
- .decision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(decision_event.id.as_str())
- );
- assert!(projection.issues.is_empty());
-
- let result = order_projection_query_for_order_id(&store, &order_id("order-1"), 10)
- .await
- .expect("projection result");
-
- assert_eq!(result.event_count, 2);
- assert_eq!(result.limit_applied, 10);
- assert_eq!(
- result
- .event_ids
- .iter()
- .map(RadrootsEventId::as_str)
- .collect::<Vec<_>>(),
- vec![request_event_id.as_str(), decision_event.id.as_str()]
- );
- assert_eq!(result.projection.status, RadrootsOrderStatus::Accepted);
- }
-
- #[cfg(feature = "event_store")]
- #[tokio::test]
- async fn order_events_for_order_id_reports_invalid_stored_tags_json() {
- let store = RadrootsEventStore::open_memory().await.expect("store");
- let request_event = store_order_request_event("order-1", 30);
- store
- .ingest_event(RadrootsEventIngest::new(request_event.clone(), 3_000))
- .await
- .expect("ingest");
- sqlx::query("UPDATE nostr_event SET tags_json = '[' WHERE event_id = ?")
- .bind(request_event.id.as_str())
- .execute(store.pool())
- .await
- .expect("corrupt tags_json");
-
- let error = order_events_for_order_id(&store, &order_id("order-1"), 10)
- .await
- .expect_err("invalid stored tags");
-
- assert!(matches!(
- error,
- RadrootsOrderStoreQueryError::InvalidStoredTagsJson { .. }
- ));
- }
-
- fn reduce_order_events<I, J, K, L, M>(
- order_id: &str,
- requests: I,
- decisions: J,
- fulfillments: K,
- cancellations: L,
- receipts: M,
- ) -> RadrootsOrderProjection
- where
- I: IntoIterator<Item = RadrootsOrderRequestRecord>,
- J: IntoIterator<Item = RadrootsOrderDecisionRecord>,
- K: IntoIterator<Item = RadrootsOrderFulfillmentRecord>,
- L: IntoIterator<Item = RadrootsOrderCancellationRecord>,
- M: IntoIterator<Item = RadrootsOrderReceiptRecord>,
- {
- let order_id = RadrootsOrderId::parse(order_id).expect("order id");
- reduce_order_events_with_revisions(
- &order_id,
- requests,
- decisions,
- Vec::<RadrootsOrderRevisionProposalRecord>::new(),
- Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
- fulfillments,
- cancellations,
- receipts,
- Vec::<RadrootsOrderPaymentEventRecord>::new(),
- Vec::<RadrootsOrderSettlementRecord>::new(),
- )
- }
-
- fn reduce_order_events_with_revisions<I, J, K, L, M, N, O, P, Q>(
- order_id: &RadrootsOrderId,
- requests: I,
- decisions: J,
- revision_proposals: K,
- revision_decisions: L,
- fulfillments: M,
- cancellations: N,
- receipts: O,
- payments: P,
- settlements: Q,
- ) -> RadrootsOrderProjection
- where
- I: IntoIterator<Item = RadrootsOrderRequestRecord>,
- J: IntoIterator<Item = RadrootsOrderDecisionRecord>,
- K: IntoIterator<Item = RadrootsOrderRevisionProposalRecord>,
- L: IntoIterator<Item = RadrootsOrderRevisionDecisionRecord>,
- M: IntoIterator<Item = RadrootsOrderFulfillmentRecord>,
- N: IntoIterator<Item = RadrootsOrderCancellationRecord>,
- O: IntoIterator<Item = RadrootsOrderReceiptRecord>,
- P: IntoIterator<Item = RadrootsOrderPaymentEventRecord>,
- Q: IntoIterator<Item = RadrootsOrderSettlementRecord>,
- {
- reduce_order_events_with_revisions_inner(
- order_id,
- RadrootsOrderReductionInputs {
- requests,
- decisions,
- revision_proposals,
- revision_decisions,
- fulfillments,
- cancellations,
- receipts,
- payments,
- settlements,
- },
- )
- }
-
- fn reduce_listing_inventory_accounting<I, J, K, L, M, N>(
- listing_addr: &str,
- listing_event_id: &str,
- bins: I,
- requests: J,
- decisions: K,
- fulfillments: L,
- cancellations: M,
- receipts: N,
- ) -> RadrootsListingInventoryAccountingProjection
- where
- I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>,
- J: IntoIterator<Item = RadrootsOrderRequestRecord>,
- K: IntoIterator<Item = RadrootsOrderDecisionRecord>,
- L: IntoIterator<Item = RadrootsOrderFulfillmentRecord>,
- M: IntoIterator<Item = RadrootsOrderCancellationRecord>,
- N: IntoIterator<Item = RadrootsOrderReceiptRecord>,
- {
- let listing_addr = RadrootsListingAddress::parse(listing_addr).expect("listing address");
- let listing_event_id = test_event_id(listing_event_id);
- reduce_listing_inventory_accounting_with_revisions(
- &listing_addr,
- &listing_event_id,
- bins,
- requests,
- decisions,
- Vec::<RadrootsOrderRevisionProposalRecord>::new(),
- Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
- fulfillments,
- cancellations,
- receipts,
- )
- }
-
- fn reduce_listing_inventory_accounting_with_revisions<I, J, K, L, M, N, O, P>(
- listing_addr: &RadrootsListingAddress,
- listing_event_id: &RadrootsEventId,
- bins: I,
- requests: J,
- decisions: K,
- revision_proposals: L,
- revision_decisions: M,
- fulfillments: N,
- cancellations: O,
- receipts: P,
- ) -> RadrootsListingInventoryAccountingProjection
- where
- I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>,
- J: IntoIterator<Item = RadrootsOrderRequestRecord>,
- K: IntoIterator<Item = RadrootsOrderDecisionRecord>,
- L: IntoIterator<Item = RadrootsOrderRevisionProposalRecord>,
- M: IntoIterator<Item = RadrootsOrderRevisionDecisionRecord>,
- N: IntoIterator<Item = RadrootsOrderFulfillmentRecord>,
- O: IntoIterator<Item = RadrootsOrderCancellationRecord>,
- P: IntoIterator<Item = RadrootsOrderReceiptRecord>,
- {
- reduce_listing_inventory_accounting_with_revisions_inner(
- listing_addr,
- listing_event_id,
- RadrootsListingInventoryAccountingInputs {
- bins,
- requests,
- decisions,
- revision_proposals,
- revision_decisions,
- fulfillments,
- cancellations,
- receipts,
- },
- )
- }
-
- #[test]
- fn canonicalize_order_request_sets_authority_and_trims_items() {
- let request =
- canonicalize_order_request_for_signer(sample_order_request("", ""), BUYER).unwrap();
-
- assert_eq!(request.order_id, "order-1");
- assert_eq!(
- request.listing_addr,
- format!("{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- );
- assert_eq!(request.buyer_pubkey, BUYER);
- assert_eq!(request.seller_pubkey, SELLER);
- assert_eq!(request.items[0].bin_id, "bin-1");
- }
-
- #[test]
- fn canonicalize_order_request_merges_duplicate_items() {
- let mut request = sample_order_request("", "");
- request.economics.total = usd("12");
- request.items = vec![
- RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 1,
- },
- RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 1,
- },
- ];
-
- let request = canonicalize_order_request_for_signer(request, BUYER).unwrap();
-
- assert_eq!(
- request.items,
- vec![RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }]
- );
- assert_eq!(request.economics.total, usd("10"));
- }
-
- #[test]
- fn canonicalize_order_request_rejects_wrong_buyer_signer() {
- let error = canonicalize_order_request_for_signer(sample_order_request(SELLER, ""), BUYER)
- .unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOrderCanonicalizationError::InvalidBuyerSigner
- ));
- }
-
- #[test]
- fn canonicalize_order_request_rejects_draft_listing_address() {
- let mut request = sample_order_request("", "");
- request.listing_addr = RadrootsListingAddress::parse(format!(
- "{KIND_LISTING_DRAFT}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("draft listing address");
-
- let error = canonicalize_order_request_for_signer(request, BUYER).unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOrderCanonicalizationError::InvalidListingKind
- ));
- }
-
- #[test]
- fn canonicalize_order_decision_sets_seller_authority_and_commitments() {
- let decision =
- canonicalize_order_decision_for_signer(sample_order_decision(""), SELLER).unwrap();
-
- assert_eq!(decision.order_id, "order-1");
- assert_eq!(
- decision.listing_addr,
- format!("{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- );
- assert_eq!(decision.buyer_pubkey, BUYER);
- assert_eq!(decision.seller_pubkey, SELLER);
- let RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments,
- } = decision.decision
- else {
- panic!("expected accepted decision")
- };
- assert_eq!(inventory_commitments[0].bin_id, "bin-1");
- }
-
- #[test]
- fn canonicalize_order_decision_rejects_wrong_seller_signer() {
- let error = canonicalize_order_decision_for_signer(sample_order_decision(BUYER), SELLER)
- .unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOrderCanonicalizationError::InvalidSellerListing
- ));
- }
-
- #[test]
- fn canonicalize_order_decision_rejects_invalid_commitments() {
- let mut decision = sample_order_decision("");
- let RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments,
- } = &mut decision.decision
- else {
- panic!("expected accepted decision")
- };
- inventory_commitments.clear();
-
- let error = canonicalize_order_decision_for_signer(decision, SELLER).unwrap_err();
- assert!(matches!(
- error,
- RadrootsOrderCanonicalizationError::MissingInventoryCommitments
- ));
- }
-
- #[test]
- fn canonicalize_order_decision_trims_decline_reason() {
- let mut decision = sample_order_decision("");
- decision.decision = RadrootsOrderDecisionOutcome::Declined {
- reason: " out_of_stock ".to_string(),
- };
-
- let decision = canonicalize_order_decision_for_signer(decision, SELLER).unwrap();
- let RadrootsOrderDecisionOutcome::Declined { reason } = decision.decision else {
- panic!("expected declined decision")
- };
- assert_eq!(reason, "out_of_stock");
- }
-
- #[test]
- fn reduce_order_events_reports_missing_without_events() {
- let projection = reduce_order_events("order-1", [], [], [], [], []);
-
- assert_eq!(projection.status, RadrootsOrderStatus::Missing);
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_reports_requested_state() {
- let projection = reduce_order_events("order-1", [request_record()], [], [], [], []);
-
- assert_eq!(projection.status, RadrootsOrderStatus::Requested);
- assert_eq!(
- projection.request_event_id.as_ref(),
- Some(&test_event_id("request-1"))
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("request-1"))
- );
- assert_eq!(
- projection.economics,
- Some(request_economics("bin-1", 2, "10"))
- );
- assert_eq!(projection.pending_revision_event_id, None);
- }
-
- #[test]
- fn reduce_order_events_reports_accepted_state() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.decision_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- assert_eq!(
- projection.fulfillment_status,
- Some(RadrootsOrderFulfillmentState::AcceptedNotFulfilled)
- );
- assert_eq!(projection.fulfillment_event_id, None);
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- assert_eq!(
- projection.economics,
- Some(request_economics("bin-1", 2, "10"))
- );
- assert_eq!(projection.pending_revision_event_id, None);
- }
-
- #[test]
- fn reduce_order_events_reports_recorded_payment_state() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- Vec::<RadrootsOrderRevisionProposalRecord>::new(),
- Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- [payment_record("payment-1", "decision-1")],
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.payment.state,
- RadrootsOrderPaymentState::Recorded
- );
- assert_eq!(
- projection.payment.settlement_state,
- RadrootsOrderSettlementState::Pending
- );
- assert_eq!(
- projection.payment.payment_event_id.as_ref(),
- Some(&test_event_id("payment-1"))
- );
- assert_eq!(
- projection.payment.agreement_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- assert_eq!(projection.payment.amount, Some(decimal("10")));
- assert_eq!(projection.payment.currency, Some(RadrootsCoreCurrency::USD));
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_reports_accepted_settlement_state() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- Vec::<RadrootsOrderRevisionProposalRecord>::new(),
- Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- [payment_record("payment-1", "decision-1")],
- [settlement_record(
- "settlement-1",
- "payment-1",
- RadrootsOrderSettlementOutcome::Accepted,
- )],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(projection.payment.state, RadrootsOrderPaymentState::Settled);
- assert_eq!(
- projection.payment.settlement_state,
- RadrootsOrderSettlementState::Accepted
- );
- assert_eq!(
- projection.payment.settlement_event_id.as_ref(),
- Some(&test_event_id("settlement-1"))
- );
- assert_eq!(projection.payment.reason, None);
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_rejects_stale_payment_amount() {
- let mut payment = payment_record("payment-1", "decision-1");
- payment.payload.amount = decimal("9");
-
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- Vec::<RadrootsOrderRevisionProposalRecord>::new(),
- Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- [payment],
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(projection.payment.state, RadrootsOrderPaymentState::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::PaymentAmountMismatch { event_id }
- if event_id == &test_event_id("payment-1")
- )));
- }
-
- #[test]
- fn reduce_order_events_keeps_payment_separate_from_receipt() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- Vec::<RadrootsOrderRevisionProposalRecord>::new(),
- Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Delivered,
- )],
- Vec::<RadrootsOrderCancellationRecord>::new(),
- [receipt_record("receipt-1", "fulfillment-1", true)],
- [payment_record("payment-1", "decision-1")],
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Completed);
- assert_eq!(projection.receipt_received, Some(true));
- assert_eq!(
- projection.payment.state,
- RadrootsOrderPaymentState::Recorded
- );
- assert_eq!(
- projection.payment.settlement_state,
- RadrootsOrderSettlementState::Pending
- );
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_applies_accepted_revision_agreement() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- [revision_proposal_record(
- "revision-proposal-1",
- "decision-1",
- "revision-1",
- 1,
- )],
- [revision_decision_record(
- "revision-decision-1",
- "revision-proposal-1",
- "revision-1",
- RadrootsOrderRevisionOutcome::Accepted,
- )],
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- Vec::<RadrootsOrderPaymentEventRecord>::new(),
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.agreement_event_id.as_ref(),
- Some(&test_event_id("revision-decision-1"))
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("revision-decision-1"))
- );
- assert_eq!(
- projection.economics,
- Some(request_economics("bin-1", 1, "5"))
- );
- assert_eq!(projection.pending_revision_event_id, None);
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_reports_pending_revision_proposal() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- [revision_proposal_record(
- "revision-proposal-1",
- "decision-1",
- "revision-1",
- 1,
- )],
- Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- Vec::<RadrootsOrderPaymentEventRecord>::new(),
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.agreement_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("revision-proposal-1"))
- );
- assert_eq!(
- projection.pending_revision_event_id.as_ref(),
- Some(&test_event_id("revision-proposal-1"))
- );
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_preserves_agreement_after_declined_revision() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- [revision_proposal_record(
- "revision-proposal-1",
- "decision-1",
- "revision-1",
- 1,
- )],
- [revision_decision_record(
- "revision-decision-1",
- "revision-proposal-1",
- "revision-1",
- RadrootsOrderRevisionOutcome::Declined {
- reason: "keep original order".to_string(),
- },
- )],
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- Vec::<RadrootsOrderPaymentEventRecord>::new(),
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.agreement_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("revision-decision-1"))
- );
- assert_eq!(
- projection.economics,
- Some(request_economics("bin-1", 2, "10"))
- );
- assert_eq!(projection.pending_revision_event_id, None);
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_continues_lifecycle_after_declined_revision() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- [revision_proposal_record(
- "revision-proposal-1",
- "decision-1",
- "revision-1",
- 1,
- )],
- [revision_decision_record(
- "revision-decision-1",
- "revision-proposal-1",
- "revision-1",
- RadrootsOrderRevisionOutcome::Declined {
- reason: "keep original order".to_string(),
- },
- )],
- [fulfillment_record(
- "fulfillment-1",
- "revision-decision-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- )],
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- Vec::<RadrootsOrderPaymentEventRecord>::new(),
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.agreement_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- assert_eq!(
- projection.fulfillment_event_id.as_ref(),
- Some(&test_event_id("fulfillment-1"))
- );
- assert_eq!(
- projection.fulfillment_status,
- Some(RadrootsOrderFulfillmentState::ReadyForPickup)
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("fulfillment-1"))
- );
- assert_eq!(projection.pending_revision_event_id, None);
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_rejects_wrong_actor_revision_decision() {
- let mut decision = revision_decision_record(
- "revision-decision-1",
- "revision-proposal-1",
- "revision-1",
- RadrootsOrderRevisionOutcome::Accepted,
- );
- decision.author_pubkey = pubkey(SELLER);
-
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- [revision_proposal_record(
- "revision-proposal-1",
- "decision-1",
- "revision-1",
- 1,
- )],
- [decision],
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- Vec::<RadrootsOrderPaymentEventRecord>::new(),
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id }
- if event_id == &test_event_id("revision-decision-1")
- )));
- }
-
- #[test]
- fn reduce_order_events_rejects_stale_revision_decision() {
- let projection = reduce_order_events_with_revisions(
- &order_id("order-1"),
- [request_record()],
- [accepted_decision_record("decision-1")],
- [revision_proposal_record(
- "revision-proposal-1",
- "decision-1",
- "revision-1",
- 1,
- )],
- [revision_decision_record(
- "revision-decision-1",
- "unknown-proposal",
- "revision-1",
- RadrootsOrderRevisionOutcome::Accepted,
- )],
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- Vec::<RadrootsOrderPaymentEventRecord>::new(),
- Vec::<RadrootsOrderSettlementRecord>::new(),
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id }
- if event_id == &test_event_id("revision-decision-1")
- )));
- }
-
- #[test]
- fn reduce_order_events_rejects_invalid_request_economics() {
- let mut request = request_record();
- request.payload.economics.total = usd("12");
-
- let projection = reduce_order_events("order-1", [request], [], [], [], []);
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(projection.economics, None);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::RequestPayloadInvalid {
- event_id: test_event_id("request-1")
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_reports_latest_fulfillment_state() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [
- fulfillment_record(
- "fulfillment-2",
- "fulfillment-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- ),
- fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Preparing,
- ),
- ],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.fulfillment_status,
- Some(RadrootsOrderFulfillmentState::ReadyForPickup)
- );
- assert_eq!(
- projection.fulfillment_event_id.as_ref(),
- Some(&test_event_id("fulfillment-2"))
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("fulfillment-2"))
- );
- }
-
- #[test]
- fn reduce_order_events_keeps_delivered_without_receipt_nonterminal() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Delivered,
- )],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert_eq!(
- projection.fulfillment_status,
- Some(RadrootsOrderFulfillmentState::Delivered)
- );
- assert!(!projection.lifecycle_terminal);
- }
-
- #[test]
- fn reduce_order_events_reports_requested_cancellation() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [],
- [],
- [cancellation_record("cancel-1", "request-1")],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Cancelled);
- assert_eq!(
- projection.request_event_id.as_ref(),
- Some(&test_event_id("request-1"))
- );
- assert_eq!(
- projection.cancellation_event_id.as_ref(),
- Some(&test_event_id("cancel-1"))
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("cancel-1"))
- );
- assert!(projection.lifecycle_terminal);
- assert_eq!(
- projection.payment,
- RadrootsOrderPaymentProjection::not_recorded()
- );
- assert!(projection.issues.is_empty());
- }
-
- #[test]
- fn reduce_order_events_rejects_request_cancellation_decision_fork() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [],
- [cancellation_record("cancel-1", "request-1")],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::ForkedLifecycle {
- event_ids: vec![test_event_id("cancel-1"), test_event_id("decision-1")]
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_reports_accepted_cancellation_before_fulfillment() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [],
- [cancellation_record("cancel-1", "decision-1")],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Cancelled);
- assert_eq!(
- projection.decision_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- assert_eq!(
- projection.cancellation_event_id.as_ref(),
- Some(&test_event_id("cancel-1"))
- );
- assert_eq!(
- projection.last_event_id.as_ref(),
- Some(&test_event_id("cancel-1"))
- );
- assert!(projection.lifecycle_terminal);
- }
-
- #[test]
- fn reduce_order_events_rejects_cancellation_fulfillment_fork() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Preparing,
- )],
- [cancellation_record("cancel-1", "decision-1")],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::ForkedLifecycle {
- event_ids: vec![test_event_id("cancel-1"), test_event_id("fulfillment-1")]
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_reports_completed_buyer_receipt() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- )],
- [],
- [receipt_record("receipt-1", "fulfillment-1", true)],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Completed);
- assert_eq!(
- projection.fulfillment_event_id.as_ref(),
- Some(&test_event_id("fulfillment-1"))
- );
- assert_eq!(
- projection.receipt_event_id.as_ref(),
- Some(&test_event_id("receipt-1"))
- );
- assert_eq!(projection.receipt_received, Some(true));
- assert_eq!(projection.receipt_issue, None);
- assert_eq!(projection.receipt_received_at, Some(1_777_665_600));
- assert!(projection.lifecycle_terminal);
- assert_eq!(
- projection.payment,
- RadrootsOrderPaymentProjection::not_recorded()
- );
- }
-
- #[test]
- fn reduce_order_events_rejects_receipt_fulfillment_fork() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [
- fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- ),
- fulfillment_record(
- "fulfillment-2",
- "fulfillment-1",
- RadrootsOrderFulfillmentState::Delivered,
- ),
- ],
- [],
- [receipt_record("receipt-1", "fulfillment-1", true)],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::ForkedLifecycle {
- event_ids: vec![test_event_id("fulfillment-2"), test_event_id("receipt-1")]
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_reports_disputed_buyer_receipt() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Delivered,
- )],
- [],
- [receipt_record("receipt-1", "fulfillment-1", false)],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Disputed);
- assert_eq!(
- projection.receipt_event_id.as_ref(),
- Some(&test_event_id("receipt-1"))
- );
- assert_eq!(projection.receipt_received, Some(false));
- assert_eq!(projection.receipt_issue.as_deref(), Some("damaged items"));
- assert!(projection.lifecycle_terminal);
- assert_eq!(
- projection.payment,
- RadrootsOrderPaymentProjection::not_recorded()
- );
- }
-
- #[test]
- fn reduce_order_events_rejects_receipt_without_eligible_fulfillment() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Preparing,
- )],
- [],
- [receipt_record("receipt-1", "fulfillment-1", true)],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::ReceiptWithoutEligibleFulfillment {
- event_id: test_event_id("receipt-1")
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_rejects_fulfillment_before_acceptance() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [],
- [fulfillment_record(
- "fulfillment-1",
- "request-1",
- RadrootsOrderFulfillmentState::Preparing,
- )],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision {
- event_id: test_event_id("fulfillment-1")
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_rejects_fulfillment_after_decline() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [declined_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Preparing,
- )],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision {
- event_id: test_event_id("fulfillment-1")
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_rejects_wrong_actor_fulfillment() {
- let mut fulfillment = fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Preparing,
- );
- fulfillment.author_pubkey = pubkey(BUYER);
-
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::FulfillmentAuthorMismatch { event_id }
- if event_id == &test_event_id("fulfillment-1")
- )));
- }
-
- #[test]
- fn reduce_order_events_rejects_forked_fulfillment_chain() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [
- fulfillment_record(
- "fulfillment-2",
- "decision-1",
- RadrootsOrderFulfillmentState::Preparing,
- ),
- fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- ),
- ],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::ForkedFulfillments {
- event_ids: vec![
- test_event_id("fulfillment-1"),
- test_event_id("fulfillment-2")
- ]
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_rejects_terminal_fulfillment_transition() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [accepted_decision_record("decision-1")],
- [
- fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Delivered,
- ),
- fulfillment_record(
- "fulfillment-2",
- "fulfillment-1",
- RadrootsOrderFulfillmentState::ReadyForPickup,
- ),
- ],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::FulfillmentUnsupportedTransition {
- event_id: test_event_id("fulfillment-2")
- }]
- );
- }
-
- #[test]
- fn reduce_order_events_reports_declined_state() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [declined_decision_record("decision-1")],
- [],
- [],
- [],
- );
-
- assert_eq!(projection.status, RadrootsOrderStatus::Declined);
- assert_eq!(
- projection.decision_event_id.as_ref(),
- Some(&test_event_id("decision-1"))
- );
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_reserves_accepted_inventory() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [accepted_decision_record("decision-1")],
- [],
- [],
- [],
- );
-
- assert_eq!(
- projection.listing_event_id,
- test_event_id("listing-event-1")
- );
- assert_eq!(projection.declined_order_ids, Vec::<RadrootsOrderId>::new());
- assert_eq!(
- projection.cancelled_order_ids,
- Vec::<RadrootsOrderId>::new()
- );
- assert_eq!(projection.invalid_event_ids, Vec::<RadrootsEventId>::new());
- assert!(projection.issues.is_empty());
- assert_eq!(
- projection.bins,
- vec![RadrootsListingInventoryBinAccounting {
- bin_id: bin_id("bin-1"),
- available_count: 5,
- accepted_reserved_count: 2,
- remaining_count: 3,
- over_reserved: false,
- accepted_orders: vec![RadrootsListingInventoryOrderReservation {
- order_id: order_id("order-1"),
- decision_event_id: test_event_id("decision-1"),
- bin_count: 2,
- }],
- }]
- );
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_reserves_accepted_revision_inventory() {
- let projection = reduce_listing_inventory_accounting_with_revisions(
- &listing_address(),
- &test_event_id("listing-event-1"),
- [inventory_bin(5)],
- [request_record()],
- [accepted_decision_record("decision-1")],
- [revision_proposal_record(
- "revision-proposal-1",
- "decision-1",
- "revision-1",
- 1,
- )],
- [revision_decision_record(
- "revision-decision-1",
- "revision-proposal-1",
- "revision-1",
- RadrootsOrderRevisionOutcome::Accepted,
- )],
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- );
-
- assert!(projection.issues.is_empty());
- assert_eq!(projection.bins[0].accepted_reserved_count, 1);
- assert_eq!(projection.bins[0].remaining_count, 4);
- assert_eq!(
- projection.bins[0].accepted_orders,
- vec![RadrootsListingInventoryOrderReservation {
- order_id: order_id("order-1"),
- decision_event_id: test_event_id("revision-decision-1"),
- bin_count: 1,
- }]
- );
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_releases_latest_seller_cancelled_order() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::SellerCancelled,
- )],
- [],
- [],
- );
-
- assert!(projection.issues.is_empty());
- assert_eq!(projection.invalid_event_ids, Vec::<RadrootsEventId>::new());
- assert_eq!(projection.bins[0].accepted_reserved_count, 0);
- assert_eq!(projection.bins[0].remaining_count, 5);
- assert!(projection.bins[0].accepted_orders.is_empty());
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_releases_accepted_buyer_cancelled_order() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [accepted_decision_record("decision-1")],
- [],
- [cancellation_record("cancel-1", "decision-1")],
- [],
- );
-
- assert!(projection.issues.is_empty());
- assert_eq!(projection.cancelled_order_ids, vec![order_id("order-1")]);
- assert_eq!(projection.invalid_event_ids, Vec::<RadrootsEventId>::new());
- assert_eq!(projection.bins[0].accepted_reserved_count, 0);
- assert_eq!(projection.bins[0].remaining_count, 5);
- assert!(projection.bins[0].accepted_orders.is_empty());
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_keeps_receipted_order_reserved() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [accepted_decision_record("decision-1")],
- [fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Delivered,
- )],
- [],
- [receipt_record("receipt-1", "fulfillment-1", true)],
- );
-
- assert!(projection.issues.is_empty());
- assert!(projection.cancelled_order_ids.is_empty());
- assert_eq!(projection.bins[0].accepted_reserved_count, 2);
- assert_eq!(projection.bins[0].remaining_count, 3);
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_rejects_forked_cancel_release() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [accepted_decision_record("decision-1")],
- [
- fulfillment_record(
- "fulfillment-2",
- "decision-1",
- RadrootsOrderFulfillmentState::SellerCancelled,
- ),
- fulfillment_record(
- "fulfillment-1",
- "decision-1",
- RadrootsOrderFulfillmentState::Preparing,
- ),
- ],
- [],
- [],
- );
-
- assert_eq!(projection.bins[0].accepted_reserved_count, 0);
- assert_eq!(
- projection.invalid_event_ids,
- vec![
- test_event_id("fulfillment-1"),
- test_event_id("fulfillment-2")
- ]
- );
- assert_eq!(
- projection.issues,
- vec![RadrootsListingInventoryAccountingIssue::InvalidOrder {
- order_id: order_id("order-1"),
- event_ids: vec![
- test_event_id("fulfillment-1"),
- test_event_id("fulfillment-2")
- ],
- }]
- );
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_leaves_declined_inventory_available() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [declined_decision_record("decision-1")],
- [],
- [],
- [],
- );
-
- assert_eq!(projection.declined_order_ids, vec![order_id("order-1")]);
- assert!(projection.cancelled_order_ids.is_empty());
- assert!(projection.invalid_event_ids.is_empty());
- assert!(projection.issues.is_empty());
- assert_eq!(projection.bins[0].accepted_reserved_count, 0);
- assert_eq!(projection.bins[0].remaining_count, 5);
- assert!(!projection.bins[0].over_reserved);
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_reports_invalid_mismatched_commitment() {
- let decision = RadrootsOrderDecisionRecord {
- payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: bin_id("bin-1"),
- bin_count: 1,
- }],
- }),
- ..accepted_decision_record("decision-1")
- };
-
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [decision],
- [],
- [],
- [],
- );
-
- assert_eq!(projection.bins[0].accepted_reserved_count, 0);
- assert_eq!(
- projection.invalid_event_ids,
- vec![test_event_id("decision-1")]
- );
- assert_eq!(
- projection.issues,
- vec![RadrootsListingInventoryAccountingIssue::InvalidOrder {
- order_id: order_id("order-1"),
- event_ids: vec![test_event_id("decision-1")],
- }]
- );
- }
-
- #[test]
- fn reduce_listing_inventory_accounting_reports_over_reserved_bins() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(3)],
- [
- request_record_for("order-2", "request-2", 2),
- request_record_for("order-1", "request-1", 2),
- ],
- [
- accepted_decision_record_for("order-2", "decision-2", "request-2", 2),
- accepted_decision_record_for("order-1", "decision-1", "request-1", 2),
- ],
- [],
- [],
- [],
- );
+fn inventory_issue_sort_key(
+ left: &RadrootsListingInventoryAccountingIssue,
+ right: &RadrootsListingInventoryAccountingIssue,
+) -> core::cmp::Ordering {
+ inventory_issue_rank(left)
+ .cmp(&inventory_issue_rank(right))
+ .then_with(|| inventory_issue_id(left).cmp(inventory_issue_id(right)))
+ .then_with(|| inventory_issue_event_ids(left).cmp(inventory_issue_event_ids(right)))
+}
- assert_eq!(projection.bins[0].available_count, 3);
- assert_eq!(projection.bins[0].accepted_reserved_count, 4);
- assert_eq!(projection.bins[0].remaining_count, 0);
- assert!(projection.bins[0].over_reserved);
- assert_eq!(
- projection.issues,
- vec![RadrootsListingInventoryAccountingIssue::OverReserved {
- bin_id: bin_id("bin-1"),
- available_count: 3,
- reserved_count: 4,
- event_ids: vec![test_event_id("decision-1"), test_event_id("decision-2")],
- }]
- );
+fn inventory_issue_rank(issue: &RadrootsListingInventoryAccountingIssue) -> u8 {
+ match issue {
+ RadrootsListingInventoryAccountingIssue::InvalidOrder { .. } => 0,
+ RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { .. } => 1,
+ RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { .. } => 2,
+ RadrootsListingInventoryAccountingIssue::OverReserved { .. } => 3,
}
+}
- #[test]
- fn reduce_listing_inventory_accounting_reports_duplicate_availability_overflow() {
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [
- RadrootsListingInventoryBinAvailability {
- bin_id: bin_id("bin-1"),
- available_count: u64::MAX,
- },
- inventory_bin(1),
- ],
- Vec::<RadrootsOrderRequestRecord>::new(),
- Vec::<RadrootsOrderDecisionRecord>::new(),
- Vec::<RadrootsOrderFulfillmentRecord>::new(),
- Vec::<RadrootsOrderCancellationRecord>::new(),
- Vec::<RadrootsOrderReceiptRecord>::new(),
- );
+fn inventory_issue_id(issue: &RadrootsListingInventoryAccountingIssue) -> &str {
+ match issue {
+ RadrootsListingInventoryAccountingIssue::InvalidOrder { order_id, .. } => order_id,
+ RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { bin_id, .. }
+ | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { bin_id, .. }
+ | RadrootsListingInventoryAccountingIssue::OverReserved { bin_id, .. } => bin_id,
+ }
+}
- assert_eq!(projection.bins[0].available_count, u64::MAX);
- assert_eq!(projection.bins[0].accepted_reserved_count, 0);
- assert_eq!(projection.bins[0].remaining_count, u64::MAX);
- assert_eq!(
- projection.issues,
- vec![
- RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
- bin_id: bin_id("bin-1"),
- event_ids: Vec::new(),
- }
- ]
- );
+fn inventory_issue_event_ids(
+ issue: &RadrootsListingInventoryAccountingIssue,
+) -> &[RadrootsEventId] {
+ match issue {
+ RadrootsListingInventoryAccountingIssue::InvalidOrder { event_ids, .. }
+ | RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { event_ids, .. }
+ | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { event_ids, .. }
+ | RadrootsListingInventoryAccountingIssue::OverReserved { event_ids, .. } => event_ids,
}
+}
- #[test]
- fn add_inventory_reservation_reports_reservation_overflow() {
- let mut bin = RadrootsListingInventoryBinAccounting {
- bin_id: bin_id("bin-1"),
- available_count: u64::MAX,
- accepted_reserved_count: u64::MAX,
- remaining_count: 0,
- over_reserved: false,
- accepted_orders: Vec::new(),
- };
- let decision = accepted_decision_record("decision-overflow");
- let mut issues = Vec::new();
-
- add_inventory_reservation(
- &mut bin,
- &order_id("order-overflow"),
- &decision,
- 1,
- &mut issues,
- );
+fn order_issue_sort_key(
+ left: &RadrootsOrderIssue,
+ right: &RadrootsOrderIssue,
+) -> core::cmp::Ordering {
+ order_issue_rank(left)
+ .cmp(&order_issue_rank(right))
+ .then_with(|| {
+ projection_issue_event_ids(core::slice::from_ref(left))
+ .cmp(&projection_issue_event_ids(core::slice::from_ref(right)))
+ })
+}
- assert_eq!(bin.accepted_reserved_count, u64::MAX);
- assert!(bin.accepted_orders.is_empty());
- assert_eq!(
- issues,
- vec![
- RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
- bin_id: bin_id("bin-1"),
- event_ids: vec![test_event_id("decision-overflow")],
- }
- ]
- );
+fn order_issue_rank(issue: &RadrootsOrderIssue) -> u8 {
+ match issue {
+ RadrootsOrderIssue::MissingRequest => 0,
+ RadrootsOrderIssue::MultipleRequests { .. } => 1,
+ RadrootsOrderIssue::RequestPayloadInvalid { .. } => 2,
+ RadrootsOrderIssue::RequestOrderIdMismatch { .. } => 3,
+ RadrootsOrderIssue::RequestAuthorMismatch { .. } => 4,
+ RadrootsOrderIssue::RequestListingAddressInvalid { .. } => 5,
+ RadrootsOrderIssue::RequestSellerListingMismatch { .. } => 6,
+ RadrootsOrderIssue::DecisionPayloadInvalid { .. } => 7,
+ RadrootsOrderIssue::DecisionOrderIdMismatch { .. } => 8,
+ RadrootsOrderIssue::DecisionAuthorMismatch { .. } => 9,
+ RadrootsOrderIssue::DecisionCounterpartyMismatch { .. } => 10,
+ RadrootsOrderIssue::DecisionBuyerMismatch { .. } => 11,
+ RadrootsOrderIssue::DecisionSellerMismatch { .. } => 12,
+ RadrootsOrderIssue::DecisionListingAddressInvalid { .. } => 13,
+ RadrootsOrderIssue::DecisionListingMismatch { .. } => 14,
+ RadrootsOrderIssue::DecisionRootMismatch { .. } => 15,
+ RadrootsOrderIssue::DecisionPreviousMismatch { .. } => 16,
+ RadrootsOrderIssue::DecisionMissingInventoryCommitments { .. } => 17,
+ RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { .. } => 18,
+ RadrootsOrderIssue::DecisionMissingReason { .. } => 19,
+ RadrootsOrderIssue::ConflictingDecisions { .. } => 20,
+ RadrootsOrderIssue::RevisionProposalPayloadInvalid { .. } => 21,
+ RadrootsOrderIssue::RevisionProposalOrderIdMismatch { .. } => 22,
+ RadrootsOrderIssue::RevisionProposalAuthorMismatch { .. } => 23,
+ RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { .. } => 24,
+ RadrootsOrderIssue::RevisionProposalBuyerMismatch { .. } => 25,
+ RadrootsOrderIssue::RevisionProposalSellerMismatch { .. } => 26,
+ RadrootsOrderIssue::RevisionProposalListingAddressInvalid { .. } => 27,
+ RadrootsOrderIssue::RevisionProposalListingMismatch { .. } => 28,
+ RadrootsOrderIssue::RevisionProposalRootMismatch { .. } => 29,
+ RadrootsOrderIssue::RevisionProposalPreviousMismatch { .. } => 30,
+ RadrootsOrderIssue::RevisionDecisionWithoutProposal { .. } => 31,
+ RadrootsOrderIssue::RevisionDecisionPayloadInvalid { .. } => 32,
+ RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { .. } => 33,
+ RadrootsOrderIssue::RevisionDecisionAuthorMismatch { .. } => 34,
+ RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { .. } => 35,
+ RadrootsOrderIssue::RevisionDecisionBuyerMismatch { .. } => 36,
+ RadrootsOrderIssue::RevisionDecisionSellerMismatch { .. } => 37,
+ RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { .. } => 38,
+ RadrootsOrderIssue::RevisionDecisionListingMismatch { .. } => 39,
+ RadrootsOrderIssue::RevisionDecisionRootMismatch { .. } => 40,
+ RadrootsOrderIssue::RevisionDecisionPreviousMismatch { .. } => 41,
+ RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { .. } => 42,
+ RadrootsOrderIssue::CancellationWithoutCancellableOrder { .. } => 43,
+ RadrootsOrderIssue::CancellationPayloadInvalid { .. } => 44,
+ RadrootsOrderIssue::CancellationOrderIdMismatch { .. } => 45,
+ RadrootsOrderIssue::CancellationAuthorMismatch { .. } => 46,
+ RadrootsOrderIssue::CancellationCounterpartyMismatch { .. } => 47,
+ RadrootsOrderIssue::CancellationBuyerMismatch { .. } => 48,
+ RadrootsOrderIssue::CancellationSellerMismatch { .. } => 49,
+ RadrootsOrderIssue::CancellationListingAddressInvalid { .. } => 50,
+ RadrootsOrderIssue::CancellationListingMismatch { .. } => 51,
+ RadrootsOrderIssue::CancellationRootMismatch { .. } => 52,
+ RadrootsOrderIssue::CancellationPreviousMismatch { .. } => 53,
+ RadrootsOrderIssue::ForkedLifecycle { .. } => 54,
}
+}
- #[test]
- fn inventory_accounting_issues_sort_by_rank_id_and_event_ids() {
- let invalid = RadrootsListingInventoryAccountingIssue::InvalidOrder {
- order_id: order_id("order-1"),
- event_ids: vec![test_event_id("event-c")],
- };
- let overflow = RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
- bin_id: bin_id("bin-1"),
- event_ids: vec![test_event_id("event-b")],
- };
- let unknown = RadrootsListingInventoryAccountingIssue::UnknownInventoryBin {
- bin_id: bin_id("bin-1"),
- event_ids: vec![test_event_id("event-a")],
- };
- let over_reserved = RadrootsListingInventoryAccountingIssue::OverReserved {
- bin_id: bin_id("bin-1"),
- available_count: 1,
- reserved_count: 2,
- event_ids: vec![test_event_id("event-d")],
- };
-
- assert_eq!(inventory_issue_rank(&invalid), 0);
- assert_eq!(inventory_issue_rank(&overflow), 1);
- assert_eq!(inventory_issue_rank(&unknown), 2);
- assert_eq!(inventory_issue_rank(&over_reserved), 3);
- assert_eq!(inventory_issue_id(&invalid), "order-1");
- assert_eq!(inventory_issue_id(&overflow), "bin-1");
- assert_eq!(inventory_issue_id(&unknown), "bin-1");
- assert_eq!(inventory_issue_id(&over_reserved), "bin-1");
- assert_eq!(
- inventory_issue_event_ids(&invalid),
- [test_event_id("event-c")]
- );
- assert_eq!(
- inventory_issue_event_ids(&overflow),
- [test_event_id("event-b")]
- );
- assert_eq!(
- inventory_issue_event_ids(&unknown),
- [test_event_id("event-a")]
- );
- assert_eq!(
- inventory_issue_event_ids(&over_reserved),
- [test_event_id("event-d")]
- );
+#[cfg(test)]
+mod tests {
+ use super::{
+ RadrootsListingInventoryAccountingInputs, RadrootsListingInventoryBinAvailability,
+ RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderEventRecord,
+ RadrootsOrderReductionInputs, RadrootsOrderRequestRecord,
+ RadrootsOrderRevisionDecisionRecord, RadrootsOrderRevisionProposalRecord,
+ RadrootsOrderStatus, reduce_listing_inventory_accounting, reduce_order_event_records,
+ reduce_order_events,
+ };
+ use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
+ };
+ use radroots_events::{
+ ids::{
+ RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
+ RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey,
+ },
+ kinds::KIND_LISTING,
+ order::{
+ RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
+ RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment,
+ RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
+ RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome,
+ RadrootsOrderRevisionProposal,
+ },
+ };
- let mut issues = vec![
- over_reserved.clone(),
- unknown.clone(),
- overflow.clone(),
- invalid.clone(),
- ];
- issues.sort_by(inventory_issue_sort_key);
+ const BUYER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
+ const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
- assert_eq!(issues, vec![invalid, overflow, unknown, over_reserved]);
+ fn event_id(raw: u8) -> RadrootsEventId {
+ RadrootsEventId::parse(format!("{raw:064x}")).expect("event id")
}
- #[test]
- fn reduce_order_events_rejects_invalid_decision_actor() {
- let mut decision = accepted_decision_record("decision-1");
- decision.author_pubkey = pubkey(BUYER);
-
- let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []);
+ fn public_key(raw: &str) -> RadrootsPublicKey {
+ RadrootsPublicKey::parse(raw).expect("public key")
+ }
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::DecisionAuthorMismatch { event_id }
- if event_id == &test_event_id("decision-1")
- )));
+ fn order_id(raw: &str) -> RadrootsOrderId {
+ RadrootsOrderId::parse(raw).expect("order id")
}
- #[test]
- fn reduce_order_events_rejects_invalid_decision_counterparty() {
- let mut decision = accepted_decision_record("decision-1");
- decision.counterparty_pubkey = pubkey(SELLER);
+ fn revision_id(raw: &str) -> RadrootsOrderRevisionId {
+ RadrootsOrderRevisionId::parse(raw).expect("revision id")
+ }
- let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []);
+ fn quote_id(raw: &str) -> RadrootsOrderQuoteId {
+ RadrootsOrderQuoteId::parse(raw).expect("quote id")
+ }
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id }
- if event_id == &test_event_id("decision-1")
- )));
+ fn bin_id(raw: &str) -> RadrootsInventoryBinId {
+ RadrootsInventoryBinId::parse(raw).expect("bin id")
}
- #[test]
- fn reduce_listing_inventory_accounting_ignores_wrong_counterparty_decision() {
- let mut decision = accepted_decision_record("decision-1");
- decision.counterparty_pubkey = pubkey(SELLER);
+ fn listing_addr() -> RadrootsListingAddress {
+ RadrootsListingAddress::parse(format!("{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg"))
+ .expect("listing address")
+ }
- let projection = reduce_listing_inventory_accounting(
- &listing_addr(),
- "listing-event-1",
- [inventory_bin(5)],
- [request_record()],
- [decision],
- [],
- [],
- [],
- );
+ fn economics(bin_count: u32) -> RadrootsOrderEconomics {
+ let currency = RadrootsCoreCurrency::USD;
+ let amount = RadrootsCoreDecimal::from(1200u32);
+ RadrootsOrderEconomics {
+ quote_id: quote_id("quote-1"),
+ quote_version: 1,
+ pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
+ currency,
+ items: vec![RadrootsOrderEconomicItem {
+ bin_id: bin_id("bin-1"),
+ bin_count,
+ quantity_amount: RadrootsCoreDecimal::ONE,
+ quantity_unit: RadrootsCoreUnit::Each,
+ unit_price_amount: amount,
+ unit_price_currency: currency,
+ line_subtotal: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(u64::from(bin_count) * 1200),
+ currency,
+ ),
+ }],
+ discounts: Vec::new(),
+ adjustments: Vec::new(),
+ subtotal: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(u64::from(bin_count) * 1200),
+ currency,
+ ),
+ discount_total: RadrootsCoreMoney::zero(currency),
+ adjustment_total: RadrootsCoreMoney::zero(currency),
+ total: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(u64::from(bin_count) * 1200),
+ currency,
+ ),
+ }
+ }
- assert_eq!(projection.bins[0].accepted_reserved_count, 0);
- assert_eq!(
- projection.invalid_event_ids,
- vec![test_event_id("decision-1")]
- );
- assert_eq!(
- projection.issues,
- vec![RadrootsListingInventoryAccountingIssue::InvalidOrder {
+ fn request_record() -> RadrootsOrderRequestRecord {
+ RadrootsOrderRequestRecord {
+ event_id: event_id(1),
+ author_pubkey: public_key(BUYER),
+ payload: RadrootsOrderRequest {
order_id: order_id("order-1"),
- event_ids: vec![test_event_id("decision-1")],
- }]
- );
+ listing_addr: listing_addr(),
+ buyer_pubkey: public_key(BUYER),
+ seller_pubkey: public_key(SELLER),
+ items: vec![RadrootsOrderItem {
+ bin_id: bin_id("bin-1"),
+ bin_count: 2,
+ }],
+ economics: economics(2),
+ },
+ }
}
- #[test]
- fn reduce_order_events_rejects_invalid_decision_chain() {
- let mut decision = accepted_decision_record("decision-1");
- decision.prev_event_id = test_event_id("request-2");
-
- let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []);
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::DecisionPreviousMismatch { event_id }
- if event_id == &test_event_id("decision-1")
- )));
+ fn accepted_decision() -> RadrootsOrderDecisionRecord {
+ RadrootsOrderDecisionRecord {
+ event_id: event_id(2),
+ author_pubkey: public_key(SELLER),
+ counterparty_pubkey: public_key(BUYER),
+ root_event_id: event_id(1),
+ prev_event_id: event_id(1),
+ payload: RadrootsOrderDecision {
+ order_id: order_id("order-1"),
+ listing_addr: listing_addr(),
+ buyer_pubkey: public_key(BUYER),
+ seller_pubkey: public_key(SELLER),
+ decision: RadrootsOrderDecisionOutcome::Accepted {
+ inventory_commitments: vec![RadrootsOrderInventoryCommitment {
+ bin_id: bin_id("bin-1"),
+ bin_count: 2,
+ }],
+ },
+ },
+ }
}
- #[test]
- fn reduce_order_events_rejects_missing_commitment() {
- let decision = RadrootsOrderDecisionRecord {
- payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: Vec::new(),
- }),
- ..accepted_decision_record("decision-1")
- };
-
- let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []);
-
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id }
- if event_id == &test_event_id("decision-1")
- )));
+ fn declined_decision() -> RadrootsOrderDecisionRecord {
+ RadrootsOrderDecisionRecord {
+ event_id: event_id(2),
+ author_pubkey: public_key(SELLER),
+ counterparty_pubkey: public_key(BUYER),
+ root_event_id: event_id(1),
+ prev_event_id: event_id(1),
+ payload: RadrootsOrderDecision {
+ order_id: order_id("order-1"),
+ listing_addr: listing_addr(),
+ buyer_pubkey: public_key(BUYER),
+ seller_pubkey: public_key(SELLER),
+ decision: RadrootsOrderDecisionOutcome::Declined {
+ reason: "not available".into(),
+ },
+ },
+ }
}
- #[test]
- fn reduce_order_events_rejects_commitment_count_mismatch() {
- let decision = RadrootsOrderDecisionRecord {
- payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
+ fn revision_proposal() -> RadrootsOrderRevisionProposalRecord {
+ RadrootsOrderRevisionProposalRecord {
+ event_id: event_id(3),
+ author_pubkey: public_key(SELLER),
+ counterparty_pubkey: public_key(BUYER),
+ root_event_id: event_id(1),
+ prev_event_id: event_id(1),
+ payload: RadrootsOrderRevisionProposal {
+ revision_id: revision_id("revision-1"),
+ order_id: order_id("order-1"),
+ listing_addr: listing_addr(),
+ buyer_pubkey: public_key(BUYER),
+ seller_pubkey: public_key(SELLER),
+ root_event_id: event_id(1),
+ prev_event_id: event_id(1),
+ items: vec![RadrootsOrderItem {
bin_id: bin_id("bin-1"),
bin_count: 1,
}],
- }),
- ..accepted_decision_record("decision-1")
- };
+ economics: economics(1),
+ reason: "one bin remains".into(),
+ },
+ }
+ }
- let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []);
+ fn accepted_revision_decision() -> RadrootsOrderRevisionDecisionRecord {
+ RadrootsOrderRevisionDecisionRecord {
+ event_id: event_id(4),
+ author_pubkey: public_key(BUYER),
+ counterparty_pubkey: public_key(SELLER),
+ root_event_id: event_id(1),
+ prev_event_id: event_id(3),
+ payload: RadrootsOrderRevisionDecision {
+ revision_id: revision_id("revision-1"),
+ order_id: order_id("order-1"),
+ listing_addr: listing_addr(),
+ buyer_pubkey: public_key(BUYER),
+ seller_pubkey: public_key(SELLER),
+ root_event_id: event_id(1),
+ prev_event_id: event_id(3),
+ decision: RadrootsOrderRevisionOutcome::Accepted,
+ },
+ }
+ }
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id }
- if event_id == &test_event_id("decision-1")
- )));
+ fn cancellation(prev_event_id: RadrootsEventId) -> RadrootsOrderCancellationRecord {
+ RadrootsOrderCancellationRecord {
+ event_id: event_id(5),
+ author_pubkey: public_key(BUYER),
+ counterparty_pubkey: public_key(SELLER),
+ root_event_id: event_id(1),
+ prev_event_id,
+ payload: RadrootsOrderCancellation {
+ order_id: order_id("order-1"),
+ listing_addr: listing_addr(),
+ buyer_pubkey: public_key(BUYER),
+ seller_pubkey: public_key(SELLER),
+ reason: "changed plans".into(),
+ },
+ }
}
- #[test]
- fn reduce_order_events_rejects_commitment_bin_mismatch() {
- let decision = RadrootsOrderDecisionRecord {
- payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: bin_id("bin-2"),
- bin_count: 2,
- }],
- }),
- ..accepted_decision_record("decision-1")
- };
+ fn reduce(
+ decisions: Vec<RadrootsOrderDecisionRecord>,
+ revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>,
+ revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>,
+ cancellations: Vec<RadrootsOrderCancellationRecord>,
+ ) -> super::RadrootsOrderProjection {
+ reduce_order_events(
+ &order_id("order-1"),
+ RadrootsOrderReductionInputs {
+ requests: vec![request_record()],
+ decisions,
+ revision_proposals,
+ revision_decisions,
+ cancellations,
+ },
+ )
+ }
- let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []);
+ #[test]
+ fn reducer_projects_requested_order() {
+ let projection = reduce(Vec::new(), Vec::new(), Vec::new(), Vec::new());
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::DecisionInventoryCommitmentMismatch {
- event_id: test_event_id("decision-1")
- }]
- );
+ assert_eq!(projection.issues, Vec::new());
+ assert_eq!(projection.status, RadrootsOrderStatus::Requested);
+ assert_eq!(projection.request_event_id, Some(event_id(1)));
+ assert!(!projection.lifecycle_terminal);
+ assert!(projection.agreement_event_id.is_none());
}
#[test]
- fn reduce_order_events_matches_normalized_duplicate_bins() {
- let mut request = request_record();
- request.payload.items = vec![
- RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 1,
- },
- RadrootsOrderItem {
- bin_id: bin_id("bin-1"),
- bin_count: 1,
- },
- ];
- let decision = RadrootsOrderDecisionRecord {
- payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: bin_id("bin-1"),
- bin_count: 2,
- }],
- }),
- ..accepted_decision_record("decision-1")
- };
-
- let projection = reduce_order_events("order-1", [request], [decision], [], [], []);
+ fn reducer_projects_accepted_order_agreement() {
+ let projection = reduce(
+ vec![accepted_decision()],
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ );
assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
- assert!(projection.issues.is_empty());
+ assert_eq!(projection.decision_event_id, Some(event_id(2)));
+ assert_eq!(projection.agreement_event_id, Some(event_id(2)));
+ assert!(projection.lifecycle_terminal);
}
#[test]
- fn reduce_order_events_rejects_missing_decline_reason() {
- let decision = RadrootsOrderDecisionRecord {
- payload: decision_payload(RadrootsOrderDecisionOutcome::Declined {
- reason: " ".to_string(),
- }),
- ..declined_decision_record("decision-1")
- };
-
- let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []);
+ fn reducer_projects_declined_order() {
+ let projection = reduce(
+ vec![declined_decision()],
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ );
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert!(projection.issues.iter().any(|issue| matches!(
- issue,
- RadrootsOrderIssue::DecisionMissingReason { event_id }
- if event_id == &test_event_id("decision-1")
- )));
+ assert_eq!(projection.status, RadrootsOrderStatus::Declined);
+ assert_eq!(projection.decision_event_id, Some(event_id(2)));
+ assert!(projection.lifecycle_terminal);
}
#[test]
- fn reduce_order_events_rejects_conflicting_decisions() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [
- accepted_decision_record("decision-2"),
- declined_decision_record("decision-1"),
- ],
- [],
- [],
- [],
+ fn reducer_projects_revision_acceptance_as_agreement() {
+ let projection = reduce(
+ Vec::new(),
+ vec![revision_proposal()],
+ vec![accepted_revision_decision()],
+ Vec::new(),
);
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
+ assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
+ assert_eq!(projection.agreement_event_id, Some(event_id(4)));
assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::ConflictingDecisions {
- event_ids: vec![test_event_id("decision-1"), test_event_id("decision-2")]
- }]
+ projection.economics.expect("economics").items[0].bin_count,
+ 1
);
+ assert!(projection.lifecycle_terminal);
}
#[test]
- fn reduce_order_events_reports_multiple_requests_deterministically() {
- let projection = reduce_order_events(
- "order-1",
- [
- request_record_with_event_id("request-2"),
- request_record_with_event_id("request-1"),
- ],
- [],
- [],
- [],
- [],
+ fn reducer_allows_pre_agreement_cancellation() {
+ let projection = reduce(
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ vec![cancellation(event_id(1))],
);
- let reversed = reduce_order_events(
- "order-1",
- [
- request_record_with_event_id("request-1"),
- request_record_with_event_id("request-2"),
- ],
- [],
- [],
- [],
- [],
+
+ assert_eq!(projection.status, RadrootsOrderStatus::Cancelled);
+ assert_eq!(projection.cancellation_event_id, Some(event_id(5)));
+ assert!(projection.lifecycle_terminal);
+ }
+
+ #[test]
+ fn reducer_rejects_cancellation_after_agreement() {
+ let projection = reduce(
+ vec![accepted_decision()],
+ Vec::new(),
+ Vec::new(),
+ vec![cancellation(event_id(2))],
);
- assert_eq!(projection, reversed);
assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.request_event_id.as_ref(),
- Some(&test_event_id("request-2"))
- );
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::MultipleRequests {
- event_ids: vec![test_event_id("request-2"), test_event_id("request-1")]
- }]
- );
+ assert!(projection.lifecycle_terminal);
}
#[test]
- fn reduce_order_events_reports_conflicting_decisions_deterministically() {
- let projection = reduce_order_events(
- "order-1",
- [request_record()],
- [
- accepted_decision_record("decision-2"),
- declined_decision_record("decision-1"),
- ],
- [],
- [],
- [],
- );
- let reversed = reduce_order_events(
- "order-1",
- [request_record()],
- [
- declined_decision_record("decision-1"),
- accepted_decision_record("decision-2"),
+ fn reducer_groups_event_records() {
+ let projection = reduce_order_event_records(
+ &order_id("order-1"),
+ vec![
+ RadrootsOrderEventRecord::Request(request_record()),
+ RadrootsOrderEventRecord::Decision(accepted_decision()),
],
- [],
- [],
- [],
);
- assert_eq!(projection, reversed);
- assert_eq!(projection.status, RadrootsOrderStatus::Invalid);
- assert_eq!(
- projection.issues,
- vec![RadrootsOrderIssue::ConflictingDecisions {
- event_ids: vec![test_event_id("decision-1"), test_event_id("decision-2")]
- }]
- );
+ assert_eq!(projection.status, RadrootsOrderStatus::Accepted);
+ assert_eq!(projection.agreement_event_id, Some(event_id(2)));
}
#[test]
- fn projection_issue_event_ids_covers_all_issue_variants() {
- macro_rules! issue {
- ($variant:ident, $id:expr) => {
- RadrootsOrderIssue::$variant {
- event_id: test_event_id($id),
- }
- };
- }
-
- let issues = vec![
- RadrootsOrderIssue::MissingRequest,
- RadrootsOrderIssue::MultipleRequests {
- event_ids: vec![test_event_id("multi-b"), test_event_id("multi-a")],
- },
- issue!(RequestPayloadInvalid, "request-payload"),
- issue!(RequestOrderIdMismatch, "request-order"),
- issue!(RequestAuthorMismatch, "request-author"),
- issue!(RequestListingAddressInvalid, "request-listing-address"),
- issue!(RequestSellerListingMismatch, "request-seller-listing"),
- issue!(DecisionPayloadInvalid, "decision-payload"),
- issue!(DecisionOrderIdMismatch, "decision-order"),
- issue!(DecisionAuthorMismatch, "decision-author"),
- issue!(DecisionCounterpartyMismatch, "decision-counterparty"),
- issue!(DecisionBuyerMismatch, "decision-buyer"),
- issue!(DecisionSellerMismatch, "decision-seller"),
- issue!(DecisionListingAddressInvalid, "decision-listing-address"),
- issue!(DecisionListingMismatch, "decision-listing"),
- issue!(DecisionRootMismatch, "decision-root"),
- issue!(DecisionPreviousMismatch, "decision-previous"),
- issue!(
- DecisionMissingInventoryCommitments,
- "decision-missing-commitments"
- ),
- issue!(
- DecisionInventoryCommitmentMismatch,
- "decision-commitment-mismatch"
- ),
- issue!(DecisionMissingReason, "decision-missing-reason"),
- RadrootsOrderIssue::ConflictingDecisions {
- event_ids: vec![test_event_id("conflict-b"), test_event_id("conflict-a")],
- },
- issue!(
- RevisionProposalWithoutAcceptedDecision,
- "proposal-without-accepted"
- ),
- issue!(RevisionProposalPayloadInvalid, "proposal-payload"),
- issue!(RevisionProposalOrderIdMismatch, "proposal-order"),
- issue!(RevisionProposalAuthorMismatch, "proposal-author"),
- issue!(
- RevisionProposalCounterpartyMismatch,
- "proposal-counterparty"
- ),
- issue!(RevisionProposalBuyerMismatch, "proposal-buyer"),
- issue!(RevisionProposalSellerMismatch, "proposal-seller"),
- issue!(
- RevisionProposalListingAddressInvalid,
- "proposal-listing-address"
- ),
- issue!(RevisionProposalListingMismatch, "proposal-listing"),
- issue!(RevisionProposalRootMismatch, "proposal-root"),
- issue!(RevisionProposalPreviousMismatch, "proposal-previous"),
- issue!(RevisionDecisionWithoutProposal, "revision-without-proposal"),
- issue!(RevisionDecisionPayloadInvalid, "revision-payload"),
- issue!(RevisionDecisionOrderIdMismatch, "revision-order"),
- issue!(RevisionDecisionAuthorMismatch, "revision-author"),
- issue!(
- RevisionDecisionCounterpartyMismatch,
- "revision-counterparty"
- ),
- issue!(RevisionDecisionBuyerMismatch, "revision-buyer"),
- issue!(RevisionDecisionSellerMismatch, "revision-seller"),
- issue!(
- RevisionDecisionListingAddressInvalid,
- "revision-listing-address"
- ),
- issue!(RevisionDecisionListingMismatch, "revision-listing"),
- issue!(RevisionDecisionRootMismatch, "revision-root"),
- issue!(RevisionDecisionPreviousMismatch, "revision-previous"),
- issue!(RevisionDecisionRevisionIdMismatch, "revision-id"),
- issue!(
- FulfillmentWithoutAcceptedDecision,
- "fulfillment-without-accepted"
- ),
- issue!(FulfillmentPayloadInvalid, "fulfillment-payload"),
- issue!(FulfillmentOrderIdMismatch, "fulfillment-order"),
- issue!(FulfillmentAuthorMismatch, "fulfillment-author"),
- issue!(FulfillmentCounterpartyMismatch, "fulfillment-counterparty"),
- issue!(FulfillmentBuyerMismatch, "fulfillment-buyer"),
- issue!(FulfillmentSellerMismatch, "fulfillment-seller"),
- issue!(
- FulfillmentListingAddressInvalid,
- "fulfillment-listing-address"
- ),
- issue!(FulfillmentListingMismatch, "fulfillment-listing"),
- issue!(FulfillmentRootMismatch, "fulfillment-root"),
- issue!(FulfillmentPreviousMismatch, "fulfillment-previous"),
- issue!(
- FulfillmentStatusNotPublishable,
- "fulfillment-not-publishable"
- ),
- issue!(
- FulfillmentUnsupportedTransition,
- "fulfillment-unsupported-transition"
- ),
- RadrootsOrderIssue::ForkedFulfillments {
- event_ids: vec![
- test_event_id("fulfillment-fork-b"),
- test_event_id("fulfillment-fork-a"),
- ],
- },
- issue!(
- CancellationWithoutCancellableOrder,
- "cancellation-without-cancellable"
- ),
- issue!(CancellationPayloadInvalid, "cancellation-payload"),
- issue!(CancellationOrderIdMismatch, "cancellation-order"),
- issue!(CancellationAuthorMismatch, "cancellation-author"),
- issue!(
- CancellationCounterpartyMismatch,
- "cancellation-counterparty"
- ),
- issue!(CancellationBuyerMismatch, "cancellation-buyer"),
- issue!(CancellationSellerMismatch, "cancellation-seller"),
- issue!(
- CancellationListingAddressInvalid,
- "cancellation-listing-address"
- ),
- issue!(CancellationListingMismatch, "cancellation-listing"),
- issue!(CancellationRootMismatch, "cancellation-root"),
- issue!(CancellationPreviousMismatch, "cancellation-previous"),
- issue!(
- CancellationAfterFulfillment,
- "cancellation-after-fulfillment"
- ),
- issue!(
- ReceiptWithoutEligibleFulfillment,
- "receipt-without-eligible"
- ),
- issue!(ReceiptPayloadInvalid, "receipt-payload"),
- issue!(ReceiptOrderIdMismatch, "receipt-order"),
- issue!(ReceiptAuthorMismatch, "receipt-author"),
- issue!(ReceiptCounterpartyMismatch, "receipt-counterparty"),
- issue!(ReceiptBuyerMismatch, "receipt-buyer"),
- issue!(ReceiptSellerMismatch, "receipt-seller"),
- issue!(ReceiptListingAddressInvalid, "receipt-listing-address"),
- issue!(ReceiptListingMismatch, "receipt-listing"),
- issue!(ReceiptRootMismatch, "receipt-root"),
- issue!(ReceiptPreviousMismatch, "receipt-previous"),
- issue!(PaymentWithoutAcceptedAgreement, "payment-without-agreement"),
- issue!(PaymentPayloadInvalid, "payment-payload"),
- issue!(PaymentOrderIdMismatch, "payment-order"),
- issue!(PaymentAuthorMismatch, "payment-author"),
- issue!(PaymentCounterpartyMismatch, "payment-counterparty"),
- issue!(PaymentBuyerMismatch, "payment-buyer"),
- issue!(PaymentSellerMismatch, "payment-seller"),
- issue!(PaymentListingAddressInvalid, "payment-listing-address"),
- issue!(PaymentListingMismatch, "payment-listing"),
- issue!(PaymentRootMismatch, "payment-root"),
- issue!(PaymentPreviousMismatch, "payment-previous"),
- issue!(PaymentAgreementMismatch, "payment-agreement"),
- issue!(PaymentQuoteMismatch, "payment-quote"),
- issue!(PaymentQuoteVersionMismatch, "payment-quote-version"),
- issue!(PaymentEconomicsDigestMismatch, "payment-digest"),
- issue!(PaymentAmountMismatch, "payment-amount"),
- issue!(PaymentCurrencyMismatch, "payment-currency"),
- issue!(PaymentAfterCancellation, "payment-after-cancellation"),
- issue!(RevisionAfterPayment, "revision-after-payment"),
- RadrootsOrderIssue::DuplicatePayments {
- event_ids: vec![
- test_event_id("payment-duplicate-b"),
- test_event_id("payment-duplicate-a"),
- ],
- },
- issue!(SettlementWithoutValidPayment, "settlement-without-payment"),
- issue!(SettlementPayloadInvalid, "settlement-payload"),
- issue!(SettlementOrderIdMismatch, "settlement-order"),
- issue!(SettlementAuthorMismatch, "settlement-author"),
- issue!(SettlementCounterpartyMismatch, "settlement-counterparty"),
- issue!(SettlementBuyerMismatch, "settlement-buyer"),
- issue!(SettlementSellerMismatch, "settlement-seller"),
- issue!(
- SettlementListingAddressInvalid,
- "settlement-listing-address"
- ),
- issue!(SettlementListingMismatch, "settlement-listing"),
- issue!(SettlementRootMismatch, "settlement-root"),
- issue!(SettlementPreviousMismatch, "settlement-previous"),
- issue!(SettlementPaymentEventMismatch, "settlement-payment-event"),
- issue!(SettlementAgreementMismatch, "settlement-agreement"),
- issue!(SettlementQuoteMismatch, "settlement-quote"),
- issue!(SettlementQuoteVersionMismatch, "settlement-quote-version"),
- issue!(SettlementEconomicsDigestMismatch, "settlement-digest"),
- issue!(SettlementAmountMismatch, "settlement-amount"),
- issue!(SettlementCurrencyMismatch, "settlement-currency"),
- RadrootsOrderIssue::DuplicateSettlements {
- event_ids: vec![
- test_event_id("settlement-duplicate-b"),
- test_event_id("settlement-duplicate-a"),
- ],
- },
- RadrootsOrderIssue::ForkedLifecycle {
- event_ids: vec![test_event_id("lifecycle-b"), test_event_id("lifecycle-a")],
+ fn inventory_accounting_reserves_only_accepted_agreements() {
+ let projection = reduce_listing_inventory_accounting(
+ &listing_addr(),
+ &event_id(9),
+ RadrootsListingInventoryAccountingInputs {
+ bins: vec![RadrootsListingInventoryBinAvailability {
+ bin_id: bin_id("bin-1"),
+ available_count: 3,
+ }],
+ requests: vec![request_record()],
+ decisions: vec![accepted_decision()],
+ revision_proposals: Vec::<RadrootsOrderRevisionProposalRecord>::new(),
+ revision_decisions: Vec::<RadrootsOrderRevisionDecisionRecord>::new(),
+ cancellations: Vec::<RadrootsOrderCancellationRecord>::new(),
},
- ];
-
- let event_ids = projection_issue_event_ids(&issues);
+ );
- assert!(event_ids.windows(2).all(|pair| pair[0] <= pair[1]));
- assert_eq!(event_ids.contains(&test_event_id("payment-digest")), true);
- assert_eq!(event_ids.contains(&test_event_id("multi-a")), true);
- assert_eq!(event_ids.contains(&test_event_id("multi-b")), true);
- assert_eq!(event_ids.contains(&test_event_id("missing-request")), false);
- assert_eq!(event_ids.len(), 126);
+ assert_eq!(projection.bins[0].accepted_reserved_count, 2);
+ assert_eq!(projection.bins[0].remaining_count, 1);
+ assert_eq!(
+ projection.bins[0].accepted_orders[0].agreement_event_id,
+ event_id(2)
+ );
}
}
diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs
@@ -8,11 +8,7 @@ use alloc::{
};
use base64::Engine as _;
-use radroots_events::{
- RadrootsNostrEvent,
- kinds::{KIND_ORDER_RECEIPT, KIND_TRADE_VALIDATION_RECEIPT},
- tags::TAG_D,
-};
+use radroots_events::{RadrootsNostrEvent, kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D};
use radroots_events_codec::wire::WireEventParts;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@@ -184,10 +180,6 @@ pub enum RadrootsValidationReceiptError {
EmptyField(&'static str),
#[error("invalid event kind {got}; expected {expected}")]
InvalidKind { expected: u32, got: u32 },
- #[error("buyer receipt kind 3434 is not a validation receipt")]
- BuyerReceiptKind,
- #[error("validation receipt kind 3440 is not a buyer receipt")]
- ValidationReceiptKind,
#[error("invalid validation receipt json")]
InvalidJson,
#[error("validation receipt json is not canonical")]
@@ -434,9 +426,6 @@ pub fn verify_validation_receipt_event(
event: &RadrootsNostrEvent,
expected: RadrootsValidationReceiptExpectedBinding<'_>,
) -> Result<RadrootsVerifiedValidationReceipt, RadrootsValidationReceiptError> {
- if event.kind == KIND_ORDER_RECEIPT {
- return Err(RadrootsValidationReceiptError::BuyerReceiptKind);
- }
if event.kind != KIND_TRADE_VALIDATION_RECEIPT {
return Err(RadrootsValidationReceiptError::InvalidKind {
expected: KIND_TRADE_VALIDATION_RECEIPT,
@@ -487,15 +476,6 @@ pub fn verify_validation_receipt_event(
Ok(RadrootsVerifiedValidationReceipt { receipt, tags })
}
-pub fn reject_validation_receipt_as_buyer_receipt(
- event: &RadrootsNostrEvent,
-) -> Result<(), RadrootsValidationReceiptError> {
- if event.kind == KIND_TRADE_VALIDATION_RECEIPT {
- return Err(RadrootsValidationReceiptError::ValidationReceiptKind);
- }
- Ok(())
-}
-
fn validate_expected_binding(
tags: &RadrootsValidationReceiptTags,
receipt: &RadrootsTradeValidationReceipt,
@@ -708,16 +688,12 @@ mod tests {
RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof,
RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult,
RadrootsValidationReceiptStatement, RadrootsValidationReceiptType,
- reject_validation_receipt_as_buyer_receipt, validation_receipt_canonical_content,
- validation_receipt_content_from_str, validation_receipt_event_build,
- validation_receipt_from_event, validation_receipt_public_values_hash_hex,
- validation_receipt_tags, verify_validation_receipt_event,
+ validation_receipt_canonical_content, validation_receipt_content_from_str,
+ validation_receipt_event_build, validation_receipt_from_event,
+ validation_receipt_public_values_hash_hex, validation_receipt_tags,
+ verify_validation_receipt_event,
};
- use radroots_events::{
- RadrootsNostrEvent,
- kinds::{KIND_ORDER_RECEIPT, KIND_TRADE_VALIDATION_RECEIPT},
- };
- use radroots_events_codec::order::order_receipt_from_event;
+ use radroots_events::{RadrootsNostrEvent, kinds::KIND_TRADE_VALIDATION_RECEIPT};
fn hash32(c: char) -> String {
format!("0x{}", c.to_string().repeat(64))
@@ -834,25 +810,16 @@ mod tests {
}
#[test]
- fn validation_receipt_verifier_rejects_buyer_receipt_kind_3434() {
+ fn validation_receipt_verifier_rejects_non_validation_receipt_kind() {
let mut event = sample_validation_receipt_event();
- event.kind = KIND_ORDER_RECEIPT;
+ event.kind = 3434;
assert_eq!(
validation_receipt_from_event(&event),
- Err(RadrootsValidationReceiptError::BuyerReceiptKind)
- );
- }
-
- #[test]
- fn validation_receipt_kind_3440_is_rejected_as_buyer_receipt() {
- let event = sample_validation_receipt_event();
- assert_eq!(
- reject_validation_receipt_as_buyer_receipt(&event),
- Err(RadrootsValidationReceiptError::ValidationReceiptKind)
+ Err(RadrootsValidationReceiptError::InvalidKind {
+ expected: KIND_TRADE_VALIDATION_RECEIPT,
+ got: 3434
+ })
);
- let buyer_receipt_error = order_receipt_from_event(&event)
- .expect_err("validation receipt must not parse as buyer receipt");
- assert!(buyer_receipt_error.to_string().contains("3440"));
}
#[test]
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -856,123 +856,6 @@ const TRADE_ORDER_CANCELLED_WITNESSES: [EventBoundarySourceWitness; 5] = [
},
];
-const TRADE_FULFILLMENT_UPDATED_WITNESSES: [EventBoundarySourceWitness; 5] = [
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/kinds.rs",
- required_fragments: &["pub const KIND_ORDER_FULFILLMENT_UPDATE: u32 = 3433;"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/order.rs",
- required_fragments: &[
- "pub struct RadrootsOrderFulfillmentUpdate",
- "Self::FulfillmentUpdated => KIND_ORDER_FULFILLMENT_UPDATE",
- ],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/encode.rs",
- required_fragments: &["pub fn order_fulfillment_update_event_build"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/decode.rs",
- required_fragments: &["pub fn order_fulfillment_update_from_event"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/trade/src/order.rs",
- required_fragments: &[
- "pub struct RadrootsOrderFulfillmentRecord",
- "pub fn reduce_order_events",
- ],
- },
-];
-
-const TRADE_BUYER_RECEIPT_WITNESSES: [EventBoundarySourceWitness; 5] = [
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/kinds.rs",
- required_fragments: &["pub const KIND_ORDER_RECEIPT: u32 = 3434;"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/order.rs",
- required_fragments: &[
- "pub struct RadrootsOrderReceipt",
- "Self::BuyerReceipt => KIND_ORDER_RECEIPT",
- ],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/encode.rs",
- required_fragments: &["pub fn order_receipt_event_build"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/decode.rs",
- required_fragments: &["pub fn order_receipt_from_event"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/trade/src/order.rs",
- required_fragments: &[
- "pub struct RadrootsOrderReceiptRecord",
- "pub fn reduce_order_events",
- ],
- },
-];
-
-const TRADE_PAYMENT_RECORDED_WITNESSES: [EventBoundarySourceWitness; 5] = [
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/kinds.rs",
- required_fragments: &["pub const KIND_ORDER_PAYMENT_RECORD: u32 = 3435;"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/order.rs",
- required_fragments: &[
- "pub struct RadrootsOrderPaymentRecord",
- "Self::PaymentRecorded => KIND_ORDER_PAYMENT_RECORD",
- ],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/encode.rs",
- required_fragments: &["pub fn order_payment_record_event_build"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/decode.rs",
- required_fragments: &["pub fn order_payment_record_from_event"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/trade/src/order.rs",
- required_fragments: &[
- "pub struct RadrootsOrderPaymentEventRecord",
- "pub fn reduce_order_events",
- ],
- },
-];
-
-const TRADE_SETTLEMENT_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = [
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/kinds.rs",
- required_fragments: &["pub const KIND_ORDER_SETTLEMENT_DECISION: u32 = 3436;"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events/src/order.rs",
- required_fragments: &[
- "pub enum RadrootsOrderSettlementOutcome",
- "pub struct RadrootsOrderSettlementDecision",
- "Self::SettlementDecision => KIND_ORDER_SETTLEMENT_DECISION",
- ],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/encode.rs",
- required_fragments: &["pub fn order_settlement_decision_event_build"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/events_codec/src/order/decode.rs",
- required_fragments: &["pub fn order_settlement_decision_from_event"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/trade/src/order.rs",
- required_fragments: &[
- "pub struct RadrootsOrderSettlementRecord",
- "pub fn reduce_order_events",
- ],
- },
-];
-
const TRADE_VALIDATION_RECEIPT_WITNESSES: [EventBoundarySourceWitness; 2] = [
EventBoundarySourceWitness {
relative_path: "crates/trade/src/validation_receipt.rs",
@@ -997,7 +880,7 @@ const RELAY_DOC_WITNESSES: [EventBoundarySourceWitness; 2] = [
},
];
-const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 45] = [
+const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 41] = [
EventBoundaryExpectation {
domain: "profile",
kind: "0",
@@ -1406,45 +1289,6 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 45] = [
witnesses: &TRADE_ORDER_CANCELLED_WITNESSES,
},
EventBoundaryExpectation {
- domain: "trade:fulfillment_updated",
- kind: "3433",
- radroots_type: "TradeFulfillmentUpdated",
- rpc_methods: &[
- "active CLI `order fulfillment update`",
- "SDK encode/decode/validate",
- "trade reducer",
- ],
- witnesses: &TRADE_FULFILLMENT_UPDATED_WITNESSES,
- },
- EventBoundaryExpectation {
- domain: "trade:buyer_receipt",
- kind: "3434",
- radroots_type: "TradeBuyerReceipt",
- rpc_methods: &[
- "active CLI `order receipt record`",
- "SDK encode/decode/validate",
- "trade reducer",
- ],
- witnesses: &TRADE_BUYER_RECEIPT_WITNESSES,
- },
- EventBoundaryExpectation {
- domain: "trade:payment_record",
- kind: "3435",
- radroots_type: "TradePaymentRecorded",
- rpc_methods: &["reserved CLI `order payment record`"],
- witnesses: &TRADE_PAYMENT_RECORDED_WITNESSES,
- },
- EventBoundaryExpectation {
- domain: "trade:settlement_decision",
- kind: "3436",
- radroots_type: "TradeSettlementDecision",
- rpc_methods: &[
- "reserved CLI `order settlement accept`",
- "reserved CLI `order settlement reject`",
- ],
- witnesses: &TRADE_SETTLEMENT_DECISION_WITNESSES,
- },
- EventBoundaryExpectation {
domain: "trade:validation_receipt",
kind: "3440",
radroots_type: "RadrootsTradeValidationReceipt",