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