Skip to main content

lastid_sdk/types/
request_status.rs

1//! Request status type definition.
2
3use serde::{Deserialize, Serialize};
4
5use super::RequestId;
6
7/// Credential request lifecycle states.
8///
9/// # State Machine
10///
11/// ```text
12/// ┌─────────┐
13/// │ Pending │◄──── Initial state after request submission
14/// └────┬────┘
15///      │
16///      ├──► Fulfilled ──────► Terminal state (success)
17///      │
18///      ├──► Denied ──────────► Terminal state (user declined)
19///      │
20///      ├──► Expired ─────────► Terminal state (request TTL exceeded)
21///      │
22///      ├──► NotFound ────────► Terminal state (request ID invalid/deleted)
23///      │
24///      └──► Timeout ─────────► Terminal state (polling timeout)
25/// ```
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(tag = "status", rename_all = "lowercase")]
28#[non_exhaustive]
29pub enum RequestStatus {
30    /// Request is pending user action
31    Pending {
32        /// Request identifier
33        request_id: RequestId,
34        /// Creation timestamp
35        created_at: String,
36    },
37
38    /// User completed the credential presentation
39    Fulfilled {
40        /// Request identifier
41        request_id: RequestId,
42        /// SD-JWT VC presentation
43        presentation: String,
44        /// Fulfillment timestamp
45        fulfilled_at: String,
46    },
47
48    /// User declined the request
49    Denied {
50        /// Request identifier
51        request_id: RequestId,
52        /// Denial reason
53        reason: String,
54        /// Denial timestamp
55        denied_at: String,
56    },
57
58    /// Request expired (IDP timeout, typically 5 minutes)
59    Expired {
60        /// Request identifier
61        request_id: RequestId,
62        /// Expiration timestamp
63        expired_at: String,
64    },
65
66    /// Request not found (invalid request ID or already deleted)
67    NotFound {
68        /// Request identifier that was not found
69        request_id: RequestId,
70        /// Error message from IDP
71        message: String,
72    },
73
74    /// SDK polling timeout reached
75    Timeout {
76        /// Request identifier
77        request_id: RequestId,
78        /// Elapsed seconds before timeout
79        elapsed_seconds: u64,
80    },
81}
82
83impl RequestStatus {
84    /// Check if this status is terminal (no further changes possible).
85    #[must_use]
86    pub const fn is_terminal(&self) -> bool {
87        !matches!(self, Self::Pending { .. })
88    }
89
90    /// Check if this status indicates success.
91    #[must_use]
92    pub const fn is_fulfilled(&self) -> bool {
93        matches!(self, Self::Fulfilled { .. })
94    }
95
96    /// Get the request ID.
97    #[must_use]
98    pub const fn request_id(&self) -> &RequestId {
99        match self {
100            Self::Pending { request_id, .. }
101            | Self::Fulfilled { request_id, .. }
102            | Self::Denied { request_id, .. }
103            | Self::Expired { request_id, .. }
104            | Self::NotFound { request_id, .. }
105            | Self::Timeout { request_id, .. } => request_id,
106        }
107    }
108
109    /// Get the presentation if fulfilled.
110    #[must_use]
111    pub fn presentation(&self) -> Option<&str> {
112        match self {
113            Self::Fulfilled { presentation, .. } => Some(presentation),
114            _ => None,
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_pending_not_terminal() {
125        let status = RequestStatus::Pending {
126            request_id: RequestId::new("req_123"),
127            created_at: "2026-01-16T12:00:00Z".into(),
128        };
129        assert!(!status.is_terminal());
130        assert!(!status.is_fulfilled());
131    }
132
133    #[test]
134    fn test_fulfilled_is_terminal() {
135        let status = RequestStatus::Fulfilled {
136            request_id: RequestId::new("req_123"),
137            presentation: "sdjwt".into(),
138            fulfilled_at: "2026-01-16T12:05:00Z".into(),
139        };
140        assert!(status.is_terminal());
141        assert!(status.is_fulfilled());
142        assert_eq!(status.presentation(), Some("sdjwt"));
143    }
144
145    #[test]
146    fn test_denied_is_terminal() {
147        let status = RequestStatus::Denied {
148            request_id: RequestId::new("req_123"),
149            reason: "User declined".into(),
150            denied_at: "2026-01-16T12:05:00Z".into(),
151        };
152        assert!(status.is_terminal());
153        assert!(!status.is_fulfilled());
154    }
155
156    #[test]
157    fn test_request_id_extraction() {
158        let status = RequestStatus::Pending {
159            request_id: RequestId::new("req_abc"),
160            created_at: "2026-01-16T12:00:00Z".into(),
161        };
162        assert_eq!(status.request_id().as_str(), "req_abc");
163    }
164
165    #[test]
166    fn test_serialize_pending() {
167        let status = RequestStatus::Pending {
168            request_id: RequestId::new("req_123"),
169            created_at: "2026-01-16T12:00:00Z".into(),
170        };
171
172        let json = serde_json::to_value(&status).unwrap();
173        assert_eq!(json["status"], "pending");
174        assert_eq!(json["request_id"], "req_123");
175    }
176
177    #[test]
178    fn test_deserialize_fulfilled() {
179        let json = r#"{
180            "status": "fulfilled",
181            "request_id": "req_123",
182            "presentation": "eyJhbGciOiJFUzI1NiJ9...",
183            "fulfilled_at": "2026-01-16T12:05:00Z"
184        }"#;
185
186        let status: RequestStatus = serde_json::from_str(json).unwrap();
187        assert!(status.is_fulfilled());
188        assert_eq!(status.request_id().as_str(), "req_123");
189    }
190
191    #[test]
192    fn test_notfound_is_terminal() {
193        let status = RequestStatus::NotFound {
194            request_id: RequestId::new("req_123"),
195            message: "Verification request not found".into(),
196        };
197        assert!(status.is_terminal());
198        assert!(!status.is_fulfilled());
199        assert_eq!(status.request_id().as_str(), "req_123");
200    }
201
202    #[test]
203    fn test_serialize_notfound() {
204        let status = RequestStatus::NotFound {
205            request_id: RequestId::new("req_404"),
206            message: "Request deleted or expired".into(),
207        };
208
209        let json = serde_json::to_value(&status).unwrap();
210        assert_eq!(json["status"], "notfound");
211        assert_eq!(json["request_id"], "req_404");
212        assert_eq!(json["message"], "Request deleted or expired");
213    }
214
215    #[test]
216    fn test_deserialize_notfound() {
217        let json = r#"{
218            "status": "notfound",
219            "request_id": "req_404",
220            "message": "Verification request not found"
221        }"#;
222
223        let status: RequestStatus = serde_json::from_str(json).unwrap();
224        assert!(status.is_terminal());
225        assert!(!status.is_fulfilled());
226        assert_eq!(status.request_id().as_str(), "req_404");
227    }
228}