Skip to main content

allsource_core/domain/value_objects/
transaction_id.rs

1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use uuid::Uuid;
5
6/// Value Object: TransactionId
7///
8/// Represents a unique identifier for a payment transaction in the paywall system.
9///
10/// Domain Rules:
11/// - Must be a valid UUID
12/// - Immutable once created
13/// - Globally unique within the system
14///
15/// This is a Value Object:
16/// - Defined by its value, not identity
17/// - Immutable
18/// - Self-validating
19/// - Compared by value equality
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct TransactionId(Uuid);
22
23impl TransactionId {
24    /// Create a new TransactionId with a random UUID
25    ///
26    /// # Examples
27    /// ```
28    /// use allsource_core::domain::value_objects::TransactionId;
29    ///
30    /// let transaction_id = TransactionId::new();
31    /// assert!(!transaction_id.is_nil());
32    /// ```
33    pub fn new() -> Self {
34        Self(Uuid::new_v4())
35    }
36
37    /// Create a TransactionId from an existing UUID
38    ///
39    /// # Examples
40    /// ```
41    /// use allsource_core::domain::value_objects::TransactionId;
42    /// use uuid::Uuid;
43    ///
44    /// let uuid = Uuid::new_v4();
45    /// let transaction_id = TransactionId::from_uuid(uuid);
46    /// assert_eq!(transaction_id.as_uuid(), uuid);
47    /// ```
48    pub fn from_uuid(uuid: Uuid) -> Self {
49        Self(uuid)
50    }
51
52    /// Parse a TransactionId from a string
53    ///
54    /// # Errors
55    /// Returns error if the string is not a valid UUID
56    ///
57    /// # Examples
58    /// ```
59    /// use allsource_core::domain::value_objects::TransactionId;
60    ///
61    /// let transaction_id = TransactionId::parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
62    /// assert_eq!(transaction_id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
63    /// ```
64    pub fn parse(value: &str) -> Result<Self> {
65        let uuid = Uuid::parse_str(value).map_err(|e| {
66            crate::error::AllSourceError::InvalidInput(format!(
67                "Invalid transaction ID '{value}': {e}"
68            ))
69        })?;
70        Ok(Self(uuid))
71    }
72
73    /// Get the UUID value
74    pub fn as_uuid(&self) -> Uuid {
75        self.0
76    }
77
78    /// Check if this is a nil (all zeros) UUID
79    pub fn is_nil(&self) -> bool {
80        self.0.is_nil()
81    }
82
83    /// Create a nil TransactionId (for testing or placeholders)
84    pub fn nil() -> Self {
85        Self(Uuid::nil())
86    }
87}
88
89impl Default for TransactionId {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl fmt::Display for TransactionId {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "{}", self.0)
98    }
99}
100
101impl From<Uuid> for TransactionId {
102    fn from(uuid: Uuid) -> Self {
103        Self(uuid)
104    }
105}
106
107impl From<TransactionId> for Uuid {
108    fn from(transaction_id: TransactionId) -> Self {
109        transaction_id.0
110    }
111}
112
113impl TryFrom<&str> for TransactionId {
114    type Error = crate::error::AllSourceError;
115
116    fn try_from(value: &str) -> Result<Self> {
117        TransactionId::parse(value)
118    }
119}
120
121impl TryFrom<String> for TransactionId {
122    type Error = crate::error::AllSourceError;
123
124    fn try_from(value: String) -> Result<Self> {
125        TransactionId::parse(&value)
126    }
127}
128
129impl AsRef<Uuid> for TransactionId {
130    fn as_ref(&self) -> &Uuid {
131        &self.0
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_create_transaction_id() {
141        let transaction_id = TransactionId::new();
142        assert!(!transaction_id.is_nil());
143    }
144
145    #[test]
146    fn test_from_uuid() {
147        let uuid = Uuid::new_v4();
148        let transaction_id = TransactionId::from_uuid(uuid);
149        assert_eq!(transaction_id.as_uuid(), uuid);
150    }
151
152    #[test]
153    fn test_parse_valid_uuid() {
154        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
155        let transaction_id = TransactionId::parse(uuid_str);
156        assert!(transaction_id.is_ok());
157        assert_eq!(transaction_id.unwrap().to_string(), uuid_str);
158    }
159
160    #[test]
161    fn test_parse_invalid_uuid() {
162        let invalid = "not-a-uuid";
163        let result = TransactionId::parse(invalid);
164        assert!(result.is_err());
165    }
166
167    #[test]
168    fn test_nil_transaction_id() {
169        let nil_id = TransactionId::nil();
170        assert!(nil_id.is_nil());
171        assert_eq!(nil_id.to_string(), "00000000-0000-0000-0000-000000000000");
172    }
173
174    #[test]
175    fn test_default_creates_new_uuid() {
176        let id1 = TransactionId::default();
177        let id2 = TransactionId::default();
178        assert_ne!(id1, id2);
179        assert!(!id1.is_nil());
180    }
181
182    #[test]
183    fn test_display_trait() {
184        let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
185        let transaction_id = TransactionId::from_uuid(uuid);
186        assert_eq!(
187            format!("{transaction_id}"),
188            "550e8400-e29b-41d4-a716-446655440000"
189        );
190    }
191
192    #[test]
193    fn test_from_uuid_trait() {
194        let uuid = Uuid::new_v4();
195        let transaction_id: TransactionId = uuid.into();
196        assert_eq!(transaction_id.as_uuid(), uuid);
197    }
198
199    #[test]
200    fn test_into_uuid_trait() {
201        let transaction_id = TransactionId::new();
202        let uuid: Uuid = transaction_id.into();
203        assert_eq!(uuid, transaction_id.as_uuid());
204    }
205
206    #[test]
207    fn test_try_from_str() {
208        let transaction_id: Result<TransactionId> =
209            "550e8400-e29b-41d4-a716-446655440000".try_into();
210        assert!(transaction_id.is_ok());
211
212        let invalid: Result<TransactionId> = "invalid".try_into();
213        assert!(invalid.is_err());
214    }
215
216    #[test]
217    fn test_try_from_string() {
218        let transaction_id: Result<TransactionId> = "550e8400-e29b-41d4-a716-446655440000"
219            .to_string()
220            .try_into();
221        assert!(transaction_id.is_ok());
222
223        let invalid: Result<TransactionId> = String::new().try_into();
224        assert!(invalid.is_err());
225    }
226
227    #[test]
228    fn test_equality() {
229        let uuid = Uuid::new_v4();
230        let id1 = TransactionId::from_uuid(uuid);
231        let id2 = TransactionId::from_uuid(uuid);
232        let id3 = TransactionId::new();
233
234        assert_eq!(id1, id2);
235        assert_ne!(id1, id3);
236    }
237
238    #[test]
239    fn test_cloning() {
240        let id1 = TransactionId::new();
241        let id2 = id1; // Copy
242        assert_eq!(id1, id2);
243    }
244
245    #[test]
246    fn test_hash_consistency() {
247        use std::collections::HashSet;
248
249        let uuid = Uuid::new_v4();
250        let id1 = TransactionId::from_uuid(uuid);
251        let id2 = TransactionId::from_uuid(uuid);
252
253        let mut set = HashSet::new();
254        set.insert(id1);
255
256        assert!(set.contains(&id2));
257    }
258
259    #[test]
260    fn test_serde_serialization() {
261        let transaction_id = TransactionId::parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
262
263        // Serialize
264        let json = serde_json::to_string(&transaction_id).unwrap();
265        assert_eq!(json, "\"550e8400-e29b-41d4-a716-446655440000\"");
266
267        // Deserialize
268        let deserialized: TransactionId = serde_json::from_str(&json).unwrap();
269        assert_eq!(deserialized, transaction_id);
270    }
271
272    #[test]
273    fn test_as_ref() {
274        let transaction_id = TransactionId::new();
275        let uuid_ref: &Uuid = transaction_id.as_ref();
276        assert_eq!(*uuid_ref, transaction_id.as_uuid());
277    }
278}