Skip to main content

stateset_core/errors/
inventory.rs

1//! Inventory-domain errors.
2
3use uuid::Uuid;
4
5/// Errors specific to inventory operations.
6///
7/// # Example
8///
9/// ```rust
10/// use stateset_core::errors::InventoryError;
11///
12/// let err = InventoryError::insufficient_stock("SKU-001", "10", "3");
13/// assert!(err.to_string().contains("SKU-001"));
14/// ```
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum InventoryError {
18    /// Inventory item not found by SKU or ID.
19    #[error("inventory item not found: {0}")]
20    ItemNotFound(String),
21
22    /// Insufficient stock for the requested quantity.
23    #[error("insufficient stock for {sku}: requested {requested}, available {available}")]
24    InsufficientStock {
25        /// The SKU that has insufficient stock.
26        sku: String,
27        /// The quantity that was requested.
28        requested: String,
29        /// The quantity that is available.
30        available: String,
31    },
32
33    /// Inventory reservation not found.
34    #[error("reservation not found: {0}")]
35    ReservationNotFound(Uuid),
36
37    /// Inventory reservation has expired.
38    #[error("reservation expired: {0}")]
39    ReservationExpired(Uuid),
40
41    /// Duplicate SKU already exists.
42    #[error("duplicate SKU: {0}")]
43    DuplicateSku(String),
44
45    /// Extensibility.
46    #[error(transparent)]
47    Other(Box<dyn std::error::Error + Send + Sync>),
48}
49
50impl InventoryError {
51    /// Convenience constructor for `ItemNotFound`.
52    #[track_caller]
53    pub fn item_not_found(id: impl Into<String>) -> Self {
54        Self::ItemNotFound(id.into())
55    }
56
57    /// Convenience constructor for `InsufficientStock`.
58    #[track_caller]
59    pub fn insufficient_stock(
60        sku: impl Into<String>,
61        requested: impl Into<String>,
62        available: impl Into<String>,
63    ) -> Self {
64        Self::InsufficientStock {
65            sku: sku.into(),
66            requested: requested.into(),
67            available: available.into(),
68        }
69    }
70
71    /// Convenience constructor for `ReservationNotFound`.
72    #[inline]
73    #[track_caller]
74    #[must_use]
75    pub const fn reservation_not_found(id: Uuid) -> Self {
76        Self::ReservationNotFound(id)
77    }
78
79    /// Convenience constructor for `ReservationExpired`.
80    #[inline]
81    #[track_caller]
82    #[must_use]
83    pub const fn reservation_expired(id: Uuid) -> Self {
84        Self::ReservationExpired(id)
85    }
86
87    /// Convenience constructor for `DuplicateSku`.
88    #[track_caller]
89    pub fn duplicate_sku(sku: impl Into<String>) -> Self {
90        Self::DuplicateSku(sku.into())
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn item_not_found_display() {
100        let err = InventoryError::item_not_found("SKU-999");
101        assert_eq!(err.to_string(), "inventory item not found: SKU-999");
102    }
103
104    #[test]
105    fn insufficient_stock_display() {
106        let err = InventoryError::insufficient_stock("WIDGET", "10", "3");
107        let msg = err.to_string();
108        assert!(msg.contains("WIDGET"));
109        assert!(msg.contains("10"));
110        assert!(msg.contains("3"));
111    }
112
113    #[test]
114    fn reservation_not_found_display() {
115        let id = Uuid::nil();
116        let err = InventoryError::reservation_not_found(id);
117        assert!(err.to_string().contains(&id.to_string()));
118    }
119
120    #[test]
121    fn converts_to_commerce_error() {
122        use crate::errors::CommerceError;
123
124        let inv_err = InventoryError::item_not_found("SKU-001");
125        let commerce_err: CommerceError = inv_err.into();
126        assert!(commerce_err.is_not_found());
127    }
128
129    #[test]
130    fn duplicate_sku_display() {
131        let err = InventoryError::duplicate_sku("DUPE-001");
132        assert_eq!(err.to_string(), "duplicate SKU: DUPE-001");
133    }
134}