Skip to main content

stateset_core/errors/
order.rs

1//! Order-domain errors.
2
3use super::StateTransitionError;
4use std::fmt;
5use uuid::Uuid;
6
7/// Errors specific to order operations.
8///
9/// Use [`OrderError`] in domain logic that only touches orders. It converts
10/// into [`CommerceError`](super::CommerceError) via `#[from]` so callers
11/// working with the top-level result type get seamless interop.
12///
13/// # Example
14///
15/// ```rust
16/// use stateset_core::errors::OrderError;
17/// use uuid::Uuid;
18///
19/// let err = OrderError::not_found(Uuid::nil());
20/// assert!(err.to_string().contains("not found"));
21/// ```
22#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum OrderError {
25    /// Order with the given ID was not found.
26    #[error("order not found: {0}")]
27    NotFound(Uuid),
28
29    /// Order cannot be cancelled in its current status.
30    #[error("order cannot be cancelled in status: {status}")]
31    CannotCancel {
32        /// The current order status that prevents cancellation.
33        status: String,
34    },
35
36    /// Order cannot be refunded.
37    #[error("order cannot be refunded: {reason}")]
38    CannotRefund {
39        /// The reason the refund was rejected.
40        reason: String,
41    },
42
43    /// Invalid status transition for an order.
44    #[error("{0}")]
45    InvalidTransition(StateTransitionError<OrderStatusLabel>),
46
47    /// Extensibility — domain callers can wrap arbitrary errors.
48    #[error(transparent)]
49    Other(Box<dyn std::error::Error + Send + Sync>),
50}
51
52impl OrderError {
53    /// Convenience constructor for `NotFound`.
54    #[inline]
55    #[track_caller]
56    #[must_use]
57    pub const fn not_found(id: Uuid) -> Self {
58        Self::NotFound(id)
59    }
60
61    /// Convenience constructor for `CannotCancel`.
62    #[track_caller]
63    pub fn cannot_cancel(status: impl fmt::Display) -> Self {
64        Self::CannotCancel { status: status.to_string() }
65    }
66
67    /// Convenience constructor for `CannotRefund`.
68    #[track_caller]
69    pub fn cannot_refund(reason: impl Into<String>) -> Self {
70        Self::CannotRefund { reason: reason.into() }
71    }
72
73    /// Create an invalid transition error from display-able status values.
74    #[track_caller]
75    pub fn invalid_transition(from: impl fmt::Display, to: impl fmt::Display) -> Self {
76        Self::InvalidTransition(StateTransitionError::new(
77            OrderStatusLabel(from.to_string()),
78            OrderStatusLabel(to.to_string()),
79        ))
80    }
81}
82
83/// Thin wrapper so `StateTransitionError` can display order status strings.
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85pub struct OrderStatusLabel(pub String);
86
87impl fmt::Display for OrderStatusLabel {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.write_str(&self.0)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn not_found_display() {
99        let id = Uuid::nil();
100        let err = OrderError::not_found(id);
101        assert!(err.to_string().contains("not found"));
102        assert!(err.to_string().contains(&id.to_string()));
103    }
104
105    #[test]
106    fn cannot_cancel_display() {
107        let err = OrderError::cannot_cancel("shipped");
108        assert!(err.to_string().contains("shipped"));
109    }
110
111    #[test]
112    fn invalid_transition_display() {
113        let err = OrderError::invalid_transition("pending", "delivered");
114        let msg = err.to_string();
115        assert!(msg.contains("pending"));
116        assert!(msg.contains("delivered"));
117    }
118
119    #[test]
120    fn converts_to_commerce_error() {
121        use crate::errors::CommerceError;
122
123        let order_err = OrderError::not_found(Uuid::nil());
124        let commerce_err: CommerceError = order_err.into();
125        assert!(commerce_err.is_not_found());
126    }
127
128    #[test]
129    fn other_variant() {
130        let io_err = std::io::Error::other("disk full");
131        let err = OrderError::Other(Box::new(io_err));
132        assert!(err.to_string().contains("disk full"));
133    }
134}