Skip to main content

circle_user_controlled_wallets/models/
user.rs

1//! User resource models for the Circle User-Controlled Wallets API.
2//!
3//! Contains request parameters and response types for user registration and
4//! management endpoints.
5
6use serde::{Deserialize, Serialize};
7
8use super::common::PageParams;
9
10// ── Enums ─────────────────────────────────────────────────────────────────────
11
12/// PIN status for an end-user.
13#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
14#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
15pub enum PinStatus {
16    /// PIN has been set and is active.
17    Enabled,
18    /// PIN has not been configured yet.
19    Unset,
20    /// PIN is locked after too many failed attempts.
21    Locked,
22}
23
24/// Overall account status of an end-user.
25#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
26#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
27pub enum EndUserStatus {
28    /// Account is active.
29    Enabled,
30    /// Account has been disabled.
31    Disabled,
32}
33
34/// Status of security question recovery for an end-user.
35#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
36#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
37pub enum SecurityQuestionStatus {
38    /// Security questions are configured.
39    Enabled,
40    /// Security questions have not been set.
41    Unset,
42    /// Security question recovery is locked.
43    Locked,
44}
45
46// ── Models ────────────────────────────────────────────────────────────────────
47
48/// PIN security details for an end-user.
49#[derive(Debug, Clone, Default, Deserialize, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct PinSecurityDetails {
52    /// Number of failed PIN attempts since last successful authentication.
53    pub failed_attempts: Option<i32>,
54    /// Date the PIN was locked.
55    pub locked_date: Option<String>,
56    /// Date the PIN lock expires.
57    pub locked_expiry_date: Option<String>,
58    /// Date of the last lock override by an admin.
59    pub last_lock_override_date: Option<String>,
60}
61
62/// An end-user record returned by the Circle API.
63#[derive(Debug, Clone, Default, Deserialize, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct EndUser {
66    /// Unique identifier for the end-user.
67    pub id: Option<String>,
68    /// ISO 8601 timestamp when the user was created.
69    pub create_date: Option<String>,
70    /// Current PIN status.
71    pub pin_status: Option<PinStatus>,
72    /// Current account status.
73    pub status: Option<EndUserStatus>,
74    /// Current security question status.
75    pub security_question_status: Option<SecurityQuestionStatus>,
76    /// Extended PIN security details (opaque JSON).
77    pub pin_details: Option<serde_json::Value>,
78    /// Extended security question details (opaque JSON).
79    pub security_question_details: Option<serde_json::Value>,
80}
81
82// ── Response wrappers ─────────────────────────────────────────────────────────
83
84/// `data` payload for list-users responses.
85#[derive(Debug, Clone, Deserialize, Serialize)]
86#[serde(rename_all = "camelCase")]
87pub struct UsersData {
88    /// List of end-users.
89    pub users: Vec<EndUser>,
90}
91
92/// Response envelope for list-users.
93#[derive(Debug, Clone, Deserialize, Serialize)]
94#[serde(rename_all = "camelCase")]
95pub struct Users {
96    /// Paginated list of end-users.
97    pub data: UsersData,
98}
99
100/// Response envelope for the `getUserByToken` endpoint.
101///
102/// The `data` field is the [`EndUser`] directly.
103#[derive(Debug, Clone, Deserialize, Serialize)]
104#[serde(rename_all = "camelCase")]
105pub struct UserResponse {
106    /// The end-user record.
107    pub data: EndUser,
108}
109
110/// Inner `data` payload for `getUserById`.
111#[derive(Debug, Clone, Deserialize, Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct GetUserByIdData {
114    /// The end-user record.
115    pub user: EndUser,
116}
117
118/// Response envelope for `getUser` (by ID).
119#[derive(Debug, Clone, Deserialize, Serialize)]
120#[serde(rename_all = "camelCase")]
121pub struct GetUserByIdResponse {
122    /// Wrapper containing the user.
123    pub data: GetUserByIdData,
124}
125
126/// Payload inside a `userToken` response.
127#[derive(Debug, Clone, Deserialize, Serialize)]
128#[serde(rename_all = "camelCase")]
129pub struct UserTokenData {
130    /// Short-lived JWT for the end-user session.
131    pub user_token: String,
132    /// Encryption key associated with the token.
133    pub encryption_key: Option<String>,
134}
135
136/// Response envelope for `getUserToken`.
137#[derive(Debug, Clone, Deserialize, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct UserTokenResponse {
140    /// Token data payload.
141    pub data: UserTokenData,
142}
143
144// ── Request bodies ────────────────────────────────────────────────────────────
145
146/// Request body for `createUser`.
147#[derive(Debug, Clone, Deserialize, Serialize)]
148#[serde(rename_all = "camelCase")]
149pub struct CreateUserRequest {
150    /// Application-defined identifier for the user.
151    pub user_id: String,
152}
153
154/// Request body for `getUserToken`.
155#[derive(Debug, Clone, Deserialize, Serialize)]
156#[serde(rename_all = "camelCase")]
157pub struct GetUserTokenRequest {
158    /// The application user ID whose token should be retrieved.
159    pub user_id: String,
160}
161
162/// Query parameters for `listUsers`.
163#[derive(Debug, Clone, Default, Deserialize, Serialize)]
164#[serde(rename_all = "camelCase")]
165pub struct ListUsersParams {
166    /// Filter by PIN status.
167    pub pin_status: Option<PinStatus>,
168    /// Pagination cursors.
169    #[serde(flatten)]
170    pub page: PageParams,
171}
172
173// ── Tests ─────────────────────────────────────────────────────────────────────
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn pin_status_screaming_snake_case() -> Result<(), Box<dyn std::error::Error>> {
181        assert_eq!(serde_json::to_string(&PinStatus::Locked)?, "\"LOCKED\"");
182        assert_eq!(serde_json::to_string(&PinStatus::Unset)?, "\"UNSET\"");
183        Ok(())
184    }
185
186    #[test]
187    fn end_user_round_trip() -> Result<(), Box<dyn std::error::Error>> {
188        let user = EndUser {
189            id: Some("user-123".to_string()),
190            create_date: Some("2024-01-01T00:00:00Z".to_string()),
191            pin_status: Some(PinStatus::Enabled),
192            status: Some(EndUserStatus::Enabled),
193            security_question_status: Some(SecurityQuestionStatus::Unset),
194            pin_details: None,
195            security_question_details: None,
196        };
197        let json = serde_json::to_string(&user)?;
198        let decoded: EndUser = serde_json::from_str(&json)?;
199        assert_eq!(decoded.id, user.id);
200        assert_eq!(decoded.pin_status, Some(PinStatus::Enabled));
201        Ok(())
202    }
203
204    #[test]
205    fn create_user_request_serializes() -> Result<(), Box<dyn std::error::Error>> {
206        let req = CreateUserRequest { user_id: "user-abc".to_string() };
207        let json = serde_json::to_string(&req)?;
208        assert!(json.contains("userId"), "expected camelCase userId in {json}");
209        Ok(())
210    }
211}