kiteconnect_async_wasm/models/auth/
user.rs

1/*!
2User profile and account information data structures.
3
4Handles user details, account types, and user preferences.
5*/
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// User profile information from the `profile` API
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct UserProfile {
13    /// User ID
14    pub user_id: String,
15
16    /// User name/display name
17    pub user_name: String,
18
19    /// User short name
20    pub user_shortname: String,
21
22    /// User type ("individual", "corporate", etc.)
23    pub user_type: String,
24
25    /// Email address
26    pub email: String,
27
28    /// Avatar URL
29    #[serde(default)]
30    pub avatar_url: Option<String>,
31
32    /// Broker identifier  
33    pub broker: String,
34
35    /// List of enabled exchanges
36    pub exchanges: Vec<String>,
37
38    /// List of enabled products
39    pub products: Vec<String>,
40
41    /// List of enabled order types
42    pub order_types: Vec<String>,
43
44    /// User metadata
45    #[serde(default)]
46    pub meta: Option<UserMeta>,
47}
48
49/// Additional user metadata
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct UserMeta {
52    /// Demat consent status
53    #[serde(default)]
54    pub demat_consent: String,
55}
56
57impl UserProfile {
58    /// Check if user has access to a specific exchange
59    pub fn has_exchange(&self, exchange: &str) -> bool {
60        self.exchanges.iter().any(|e| e == exchange)
61    }
62
63    /// Check if user has access to a specific product
64    pub fn has_product(&self, product: &str) -> bool {
65        self.products.iter().any(|p| p == product)
66    }
67
68    /// Check if user has access to a specific order type
69    pub fn has_order_type(&self, order_type: &str) -> bool {
70        self.order_types.iter().any(|o| o == order_type)
71    }
72
73    /// Get display name (prefer user_name, fallback to user_shortname)
74    pub fn display_name(&self) -> &str {
75        if !self.user_name.is_empty() {
76            &self.user_name
77        } else {
78            &self.user_shortname
79        }
80    }
81
82    /// Check if profile has essential information
83    pub fn is_complete(&self) -> bool {
84        !self.user_id.is_empty() && !self.email.is_empty() && !self.exchanges.is_empty()
85    }
86}
87
88/// User type enumeration for type-safe handling
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "lowercase")]
91pub enum UserType {
92    Individual,
93    Corporate,
94    Partnership,
95    Huf, // Hindu Undivided Family
96}
97
98impl UserType {
99    /// Check if user type supports specific features
100    pub fn supports_family_account(&self) -> bool {
101        matches!(self, UserType::Huf)
102    }
103
104    /// Check if user type is individual
105    pub fn is_individual(&self) -> bool {
106        matches!(self, UserType::Individual)
107    }
108
109    /// Check if user type is business-related
110    pub fn is_business(&self) -> bool {
111        matches!(self, UserType::Corporate | UserType::Partnership)
112    }
113}
114
115impl std::fmt::Display for UserType {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match self {
118            UserType::Individual => write!(f, "individual"),
119            UserType::Corporate => write!(f, "corporate"),
120            UserType::Partnership => write!(f, "partnership"),
121            UserType::Huf => write!(f, "huf"),
122        }
123    }
124}
125
126impl From<String> for UserType {
127    fn from(s: String) -> Self {
128        match s.to_lowercase().as_str() {
129            "individual" => UserType::Individual,
130            "corporate" => UserType::Corporate,
131            "partnership" => UserType::Partnership,
132            "huf" => UserType::Huf,
133            _ => UserType::Individual, // Default fallback
134        }
135    }
136}
137
138impl From<&str> for UserType {
139    fn from(s: &str) -> Self {
140        Self::from(s.to_string())
141    }
142}
143
144/// Account status information
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct AccountStatus {
147    /// Account is active
148    pub active: bool,
149
150    /// Trading is enabled
151    pub trading_enabled: bool,
152
153    /// Account creation date
154    #[serde(default)]
155    pub created_at: Option<DateTime<Utc>>,
156
157    /// Last login timestamp
158    #[serde(default)]
159    pub last_login: Option<DateTime<Utc>>,
160
161    /// Account restrictions (if any)
162    #[serde(default)]
163    pub restrictions: Vec<String>,
164
165    /// KYC status
166    #[serde(default)]
167    pub kyc_status: Option<String>,
168}
169
170impl AccountStatus {
171    /// Check if account can place trades
172    pub fn can_trade(&self) -> bool {
173        self.active && self.trading_enabled && self.restrictions.is_empty()
174    }
175
176    /// Check if account has any restrictions
177    pub fn has_restrictions(&self) -> bool {
178        !self.restrictions.is_empty()
179    }
180
181    /// Check if KYC is complete
182    pub fn is_kyc_complete(&self) -> bool {
183        self.kyc_status
184            .as_ref()
185            .map(|status| status.to_lowercase() == "complete")
186            .unwrap_or(false)
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_user_profile() {
196        let profile = UserProfile {
197            user_id: "TEST123".to_string(),
198            user_name: "Test User".to_string(),
199            user_shortname: "testuser".to_string(),
200            user_type: "individual".to_string(),
201            email: "test@example.com".to_string(),
202            avatar_url: None,
203            broker: "ZERODHA".to_string(),
204            exchanges: vec!["NSE".to_string(), "BSE".to_string()],
205            products: vec!["CNC".to_string(), "MIS".to_string()],
206            order_types: vec!["MARKET".to_string(), "LIMIT".to_string()],
207            meta: None,
208        };
209
210        assert!(profile.is_complete());
211        assert!(profile.has_exchange("NSE"));
212        assert!(!profile.has_exchange("MCX"));
213        assert_eq!(profile.display_name(), "Test User");
214    }
215
216    #[test]
217    fn test_user_type() {
218        let individual = UserType::Individual;
219        assert!(individual.is_individual());
220        assert!(!individual.is_business());
221        assert!(!individual.supports_family_account());
222
223        let corporate = UserType::Corporate;
224        assert!(!corporate.is_individual());
225        assert!(corporate.is_business());
226
227        let huf = UserType::Huf;
228        assert!(huf.supports_family_account());
229
230        // Test string conversion
231        assert_eq!(UserType::from("individual"), UserType::Individual);
232        assert_eq!(UserType::from("corporate"), UserType::Corporate);
233        assert_eq!(UserType::from("unknown"), UserType::Individual); // Default fallback
234    }
235
236    #[test]
237    fn test_account_status() {
238        let mut status = AccountStatus {
239            active: true,
240            trading_enabled: true,
241            created_at: None,
242            last_login: None,
243            restrictions: vec![],
244            kyc_status: Some("complete".to_string()),
245        };
246
247        assert!(status.can_trade());
248        assert!(!status.has_restrictions());
249        assert!(status.is_kyc_complete());
250
251        // Add restriction
252        status.restrictions.push("day_trading_disabled".to_string());
253        assert!(!status.can_trade());
254        assert!(status.has_restrictions());
255
256        // Test KYC status
257        status.kyc_status = Some("pending".to_string());
258        assert!(!status.is_kyc_complete());
259    }
260}