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 '{}': {}",
68                value, e
69            ))
70        })?;
71        Ok(Self(uuid))
72    }
73
74    /// Get the UUID value
75    pub fn as_uuid(&self) -> Uuid {
76        self.0
77    }
78
79    /// Check if this is a nil (all zeros) UUID
80    pub fn is_nil(&self) -> bool {
81        self.0.is_nil()
82    }
83
84    /// Create a nil TransactionId (for testing or placeholders)
85    pub fn nil() -> Self {
86        Self(Uuid::nil())
87    }
88}
89
90impl Default for TransactionId {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl fmt::Display for TransactionId {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "{}", self.0)
99    }
100}
101
102impl From<Uuid> for TransactionId {
103    fn from(uuid: Uuid) -> Self {
104        Self(uuid)
105    }
106}
107
108impl From<TransactionId> for Uuid {
109    fn from(transaction_id: TransactionId) -> Self {
110        transaction_id.0
111    }
112}
113
114impl TryFrom<&str> for TransactionId {
115    type Error = crate::error::AllSourceError;
116
117    fn try_from(value: &str) -> Result<Self> {
118        TransactionId::parse(value)
119    }
120}
121
122impl TryFrom<String> for TransactionId {
123    type Error = crate::error::AllSourceError;
124
125    fn try_from(value: String) -> Result<Self> {
126        TransactionId::parse(&value)
127    }
128}
129
130impl AsRef<Uuid> for TransactionId {
131    fn as_ref(&self) -> &Uuid {
132        &self.0
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_create_transaction_id() {
142        let transaction_id = TransactionId::new();
143        assert!(!transaction_id.is_nil());
144    }
145
146    #[test]
147    fn test_from_uuid() {
148        let uuid = Uuid::new_v4();
149        let transaction_id = TransactionId::from_uuid(uuid);
150        assert_eq!(transaction_id.as_uuid(), uuid);
151    }
152
153    #[test]
154    fn test_parse_valid_uuid() {
155        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
156        let transaction_id = TransactionId::parse(uuid_str);
157        assert!(transaction_id.is_ok());
158        assert_eq!(transaction_id.unwrap().to_string(), uuid_str);
159    }
160
161    #[test]
162    fn test_parse_invalid_uuid() {
163        let invalid = "not-a-uuid";
164        let result = TransactionId::parse(invalid);
165        assert!(result.is_err());
166    }
167
168    #[test]
169    fn test_nil_transaction_id() {
170        let nil_id = TransactionId::nil();
171        assert!(nil_id.is_nil());
172        assert_eq!(nil_id.to_string(), "00000000-0000-0000-0000-000000000000");
173    }
174
175    #[test]
176    fn test_default_creates_new_uuid() {
177        let id1 = TransactionId::default();
178        let id2 = TransactionId::default();
179        assert_ne!(id1, id2);
180        assert!(!id1.is_nil());
181    }
182
183    #[test]
184    fn test_display_trait() {
185        let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
186        let transaction_id = TransactionId::from_uuid(uuid);
187        assert_eq!(
188            format!("{}", transaction_id),
189            "550e8400-e29b-41d4-a716-446655440000"
190        );
191    }
192
193    #[test]
194    fn test_from_uuid_trait() {
195        let uuid = Uuid::new_v4();
196        let transaction_id: TransactionId = uuid.into();
197        assert_eq!(transaction_id.as_uuid(), uuid);
198    }
199
200    #[test]
201    fn test_into_uuid_trait() {
202        let transaction_id = TransactionId::new();
203        let uuid: Uuid = transaction_id.into();
204        assert_eq!(uuid, transaction_id.as_uuid());
205    }
206
207    #[test]
208    fn test_try_from_str() {
209        let transaction_id: Result<TransactionId> =
210            "550e8400-e29b-41d4-a716-446655440000".try_into();
211        assert!(transaction_id.is_ok());
212
213        let invalid: Result<TransactionId> = "invalid".try_into();
214        assert!(invalid.is_err());
215    }
216
217    #[test]
218    fn test_try_from_string() {
219        let transaction_id: Result<TransactionId> = "550e8400-e29b-41d4-a716-446655440000"
220            .to_string()
221            .try_into();
222        assert!(transaction_id.is_ok());
223
224        let invalid: Result<TransactionId> = String::new().try_into();
225        assert!(invalid.is_err());
226    }
227
228    #[test]
229    fn test_equality() {
230        let uuid = Uuid::new_v4();
231        let id1 = TransactionId::from_uuid(uuid);
232        let id2 = TransactionId::from_uuid(uuid);
233        let id3 = TransactionId::new();
234
235        assert_eq!(id1, id2);
236        assert_ne!(id1, id3);
237    }
238
239    #[test]
240    fn test_cloning() {
241        let id1 = TransactionId::new();
242        let id2 = id1; // Copy
243        assert_eq!(id1, id2);
244    }
245
246    #[test]
247    fn test_hash_consistency() {
248        use std::collections::HashSet;
249
250        let uuid = Uuid::new_v4();
251        let id1 = TransactionId::from_uuid(uuid);
252        let id2 = TransactionId::from_uuid(uuid);
253
254        let mut set = HashSet::new();
255        set.insert(id1);
256
257        assert!(set.contains(&id2));
258    }
259
260    #[test]
261    fn test_serde_serialization() {
262        let transaction_id = TransactionId::parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
263
264        // Serialize
265        let json = serde_json::to_string(&transaction_id).unwrap();
266        assert_eq!(json, "\"550e8400-e29b-41d4-a716-446655440000\"");
267
268        // Deserialize
269        let deserialized: TransactionId = serde_json::from_str(&json).unwrap();
270        assert_eq!(deserialized, transaction_id);
271    }
272
273    #[test]
274    fn test_as_ref() {
275        let transaction_id = TransactionId::new();
276        let uuid_ref: &Uuid = transaction_id.as_ref();
277        assert_eq!(*uuid_ref, transaction_id.as_uuid());
278    }
279}