deribit_fix/message/user/
user_request.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 12/8/25
5******************************************************************************/
6
7//! User Request FIX Message Implementation
8
9use crate::error::Result as DeribitFixResult;
10use crate::message::builder::MessageBuilder;
11use crate::model::types::MsgType;
12use base64::{Engine as _, engine::general_purpose};
13use chrono::Utc;
14use deribit_base::{impl_json_debug_pretty, impl_json_display};
15use serde::{Deserialize, Serialize};
16
17/// User request type enumeration
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum UserRequestType {
20    /// Log on user
21    LogOnUser,
22    /// Log off user
23    LogOffUser,
24    /// Change password for user
25    ChangePasswordForUser,
26    /// Request individual user status
27    RequestIndividualUserStatus,
28}
29
30impl From<UserRequestType> for i32 {
31    fn from(request_type: UserRequestType) -> Self {
32        match request_type {
33            UserRequestType::LogOnUser => 1,
34            UserRequestType::LogOffUser => 2,
35            UserRequestType::ChangePasswordForUser => 3,
36            UserRequestType::RequestIndividualUserStatus => 4,
37        }
38    }
39}
40
41impl TryFrom<i32> for UserRequestType {
42    type Error = String;
43
44    fn try_from(value: i32) -> Result<Self, Self::Error> {
45        match value {
46            1 => Ok(UserRequestType::LogOnUser),
47            2 => Ok(UserRequestType::LogOffUser),
48            3 => Ok(UserRequestType::ChangePasswordForUser),
49            4 => Ok(UserRequestType::RequestIndividualUserStatus),
50            _ => Err(format!("Invalid UserRequestType: {}", value)),
51        }
52    }
53}
54
55/// User status enumeration
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum UserStatus {
58    /// Logged in
59    LoggedIn,
60    /// Not logged in
61    NotLoggedIn,
62    /// User not recognised
63    UserNotRecognised,
64    /// Password incorrect
65    PasswordIncorrect,
66    /// Password changed
67    PasswordChanged,
68    /// Other
69    Other,
70}
71
72impl From<UserStatus> for i32 {
73    fn from(status: UserStatus) -> Self {
74        match status {
75            UserStatus::LoggedIn => 1,
76            UserStatus::NotLoggedIn => 2,
77            UserStatus::UserNotRecognised => 3,
78            UserStatus::PasswordIncorrect => 4,
79            UserStatus::PasswordChanged => 5,
80            UserStatus::Other => 99,
81        }
82    }
83}
84
85impl TryFrom<i32> for UserStatus {
86    type Error = String;
87
88    fn try_from(value: i32) -> Result<Self, Self::Error> {
89        match value {
90            1 => Ok(UserStatus::LoggedIn),
91            2 => Ok(UserStatus::NotLoggedIn),
92            3 => Ok(UserStatus::UserNotRecognised),
93            4 => Ok(UserStatus::PasswordIncorrect),
94            5 => Ok(UserStatus::PasswordChanged),
95            99 => Ok(UserStatus::Other),
96            _ => Err(format!("Invalid UserStatus: {}", value)),
97        }
98    }
99}
100
101/// User Request message (MsgType = 'BE')
102#[derive(Clone, PartialEq, Serialize, Deserialize)]
103pub struct UserRequest {
104    /// User request ID
105    pub user_request_id: String,
106    /// User request type
107    pub user_request_type: UserRequestType,
108    /// Username
109    pub username: String,
110    /// Password
111    pub password: Option<String>,
112    /// New password
113    pub new_password: Option<String>,
114    /// Raw data length
115    pub raw_data_length: Option<i32>,
116    /// Raw data
117    pub raw_data: Option<Vec<u8>>,
118    /// User status
119    pub user_status: Option<UserStatus>,
120    /// User status text
121    pub user_status_text: Option<String>,
122    /// Custom label
123    pub deribit_label: Option<String>,
124}
125
126impl UserRequest {
127    /// Create a new user request
128    pub fn new(
129        user_request_id: String,
130        user_request_type: UserRequestType,
131        username: String,
132    ) -> Self {
133        Self {
134            user_request_id,
135            user_request_type,
136            username,
137            password: None,
138            new_password: None,
139            raw_data_length: None,
140            raw_data: None,
141            user_status: None,
142            user_status_text: None,
143            deribit_label: None,
144        }
145    }
146
147    /// Create a log on user request
148    pub fn log_on_user(user_request_id: String, username: String, password: String) -> Self {
149        let mut request = Self::new(user_request_id, UserRequestType::LogOnUser, username);
150        request.password = Some(password);
151        request
152    }
153
154    /// Create a log off user request
155    pub fn log_off_user(user_request_id: String, username: String) -> Self {
156        Self::new(user_request_id, UserRequestType::LogOffUser, username)
157    }
158
159    /// Create a change password request
160    pub fn change_password(
161        user_request_id: String,
162        username: String,
163        old_password: String,
164        new_password: String,
165    ) -> Self {
166        let mut request = Self::new(
167            user_request_id,
168            UserRequestType::ChangePasswordForUser,
169            username,
170        );
171        request.password = Some(old_password);
172        request.new_password = Some(new_password);
173        request
174    }
175
176    /// Create a status request
177    pub fn status_request(user_request_id: String, username: String) -> Self {
178        Self::new(
179            user_request_id,
180            UserRequestType::RequestIndividualUserStatus,
181            username,
182        )
183    }
184
185    /// Set password
186    pub fn with_password(mut self, password: String) -> Self {
187        self.password = Some(password);
188        self
189    }
190
191    /// Set new password
192    pub fn with_new_password(mut self, new_password: String) -> Self {
193        self.new_password = Some(new_password);
194        self
195    }
196
197    /// Set raw data
198    pub fn with_raw_data(mut self, raw_data: Vec<u8>) -> Self {
199        self.raw_data_length = Some(raw_data.len() as i32);
200        self.raw_data = Some(raw_data);
201        self
202    }
203
204    /// Set user status
205    pub fn with_user_status(mut self, user_status: UserStatus) -> Self {
206        self.user_status = Some(user_status);
207        self
208    }
209
210    /// Set user status text
211    pub fn with_user_status_text(mut self, user_status_text: String) -> Self {
212        self.user_status_text = Some(user_status_text);
213        self
214    }
215
216    /// Set custom label
217    pub fn with_label(mut self, label: String) -> Self {
218        self.deribit_label = Some(label);
219        self
220    }
221
222    /// Convert to FIX message
223    pub fn to_fix_message(
224        &self,
225        sender_comp_id: &str,
226        target_comp_id: &str,
227        msg_seq_num: u32,
228    ) -> DeribitFixResult<String> {
229        let mut builder = MessageBuilder::new()
230            .msg_type(MsgType::UserRequest)
231            .sender_comp_id(sender_comp_id.to_string())
232            .target_comp_id(target_comp_id.to_string())
233            .msg_seq_num(msg_seq_num)
234            .sending_time(Utc::now());
235
236        // Required fields
237        builder = builder
238            .field(923, self.user_request_id.clone()) // UserRequestID
239            .field(924, i32::from(self.user_request_type).to_string()) // UserRequestType
240            .field(553, self.username.clone()); // Username
241
242        // Optional fields
243        if let Some(password) = &self.password {
244            builder = builder.field(554, password.clone());
245        }
246
247        if let Some(new_password) = &self.new_password {
248            builder = builder.field(925, new_password.clone());
249        }
250
251        if let Some(raw_data_length) = &self.raw_data_length {
252            builder = builder.field(95, raw_data_length.to_string());
253        }
254
255        if let Some(raw_data) = &self.raw_data {
256            // Convert raw data to base64 for FIX transmission
257            let encoded_data = general_purpose::STANDARD.encode(raw_data);
258            builder = builder.field(96, encoded_data);
259        }
260
261        if let Some(user_status) = &self.user_status {
262            builder = builder.field(926, i32::from(*user_status).to_string());
263        }
264
265        if let Some(user_status_text) = &self.user_status_text {
266            builder = builder.field(927, user_status_text.clone());
267        }
268
269        if let Some(deribit_label) = &self.deribit_label {
270            builder = builder.field(100010, deribit_label.clone());
271        }
272
273        Ok(builder.build()?.to_string())
274    }
275}
276
277impl_json_display!(UserRequest);
278impl_json_debug_pretty!(UserRequest);
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_user_request_creation() {
286        let request = UserRequest::new(
287            "UR123".to_string(),
288            UserRequestType::RequestIndividualUserStatus,
289            "testuser".to_string(),
290        );
291
292        assert_eq!(request.user_request_id, "UR123");
293        assert_eq!(
294            request.user_request_type,
295            UserRequestType::RequestIndividualUserStatus
296        );
297        assert_eq!(request.username, "testuser");
298        assert!(request.password.is_none());
299        assert!(request.new_password.is_none());
300    }
301
302    #[test]
303    fn test_user_request_log_on() {
304        let request = UserRequest::log_on_user(
305            "UR456".to_string(),
306            "user1".to_string(),
307            "password123".to_string(),
308        );
309
310        assert_eq!(request.user_request_type, UserRequestType::LogOnUser);
311        assert_eq!(request.username, "user1");
312        assert_eq!(request.password, Some("password123".to_string()));
313    }
314
315    #[test]
316    fn test_user_request_log_off() {
317        let request = UserRequest::log_off_user("UR789".to_string(), "user2".to_string());
318
319        assert_eq!(request.user_request_type, UserRequestType::LogOffUser);
320        assert_eq!(request.username, "user2");
321        assert!(request.password.is_none());
322    }
323
324    #[test]
325    fn test_user_request_change_password() {
326        let request = UserRequest::change_password(
327            "UR999".to_string(),
328            "user3".to_string(),
329            "oldpass".to_string(),
330            "newpass".to_string(),
331        );
332
333        assert_eq!(
334            request.user_request_type,
335            UserRequestType::ChangePasswordForUser
336        );
337        assert_eq!(request.username, "user3");
338        assert_eq!(request.password, Some("oldpass".to_string()));
339        assert_eq!(request.new_password, Some("newpass".to_string()));
340    }
341
342    #[test]
343    fn test_user_request_status_request() {
344        let request = UserRequest::status_request("UR111".to_string(), "user4".to_string());
345
346        assert_eq!(
347            request.user_request_type,
348            UserRequestType::RequestIndividualUserStatus
349        );
350        assert_eq!(request.username, "user4");
351    }
352
353    #[test]
354    fn test_user_request_with_options() {
355        let raw_data = vec![1, 2, 3, 4, 5];
356        let request = UserRequest::new(
357            "UR222".to_string(),
358            UserRequestType::LogOnUser,
359            "user5".to_string(),
360        )
361        .with_password("mypass".to_string())
362        .with_raw_data(raw_data.clone())
363        .with_user_status(UserStatus::LoggedIn)
364        .with_user_status_text("User logged in successfully".to_string())
365        .with_label("test-user-request".to_string());
366
367        assert_eq!(request.password, Some("mypass".to_string()));
368        assert_eq!(request.raw_data, Some(raw_data));
369        assert_eq!(request.raw_data_length, Some(5));
370        assert_eq!(request.user_status, Some(UserStatus::LoggedIn));
371        assert_eq!(
372            request.user_status_text,
373            Some("User logged in successfully".to_string())
374        );
375        assert_eq!(request.deribit_label, Some("test-user-request".to_string()));
376    }
377
378    #[test]
379    fn test_user_request_to_fix_message() {
380        let request = UserRequest::log_on_user(
381            "UR123".to_string(),
382            "testuser".to_string(),
383            "secret".to_string(),
384        )
385        .with_label("test-label".to_string());
386
387        let fix_message = request.to_fix_message("SENDER", "TARGET", 1).unwrap();
388
389        // Check that the message contains required fields
390        assert!(fix_message.contains("35=BE")); // MsgType
391        assert!(fix_message.contains("923=UR123")); // UserRequestID
392        assert!(fix_message.contains("924=1")); // UserRequestType=LogOnUser
393        assert!(fix_message.contains("553=testuser")); // Username
394        assert!(fix_message.contains("554=secret")); // Password
395        assert!(fix_message.contains("100010=test-label")); // Custom label
396    }
397
398    #[test]
399    fn test_user_request_type_conversions() {
400        assert_eq!(i32::from(UserRequestType::LogOnUser), 1);
401        assert_eq!(i32::from(UserRequestType::LogOffUser), 2);
402        assert_eq!(i32::from(UserRequestType::ChangePasswordForUser), 3);
403        assert_eq!(i32::from(UserRequestType::RequestIndividualUserStatus), 4);
404
405        assert_eq!(
406            UserRequestType::try_from(1).unwrap(),
407            UserRequestType::LogOnUser
408        );
409        assert_eq!(
410            UserRequestType::try_from(2).unwrap(),
411            UserRequestType::LogOffUser
412        );
413        assert_eq!(
414            UserRequestType::try_from(3).unwrap(),
415            UserRequestType::ChangePasswordForUser
416        );
417        assert_eq!(
418            UserRequestType::try_from(4).unwrap(),
419            UserRequestType::RequestIndividualUserStatus
420        );
421
422        assert!(UserRequestType::try_from(99).is_err());
423    }
424
425    #[test]
426    fn test_user_status_conversions() {
427        assert_eq!(i32::from(UserStatus::LoggedIn), 1);
428        assert_eq!(i32::from(UserStatus::NotLoggedIn), 2);
429        assert_eq!(i32::from(UserStatus::UserNotRecognised), 3);
430        assert_eq!(i32::from(UserStatus::PasswordIncorrect), 4);
431        assert_eq!(i32::from(UserStatus::PasswordChanged), 5);
432        assert_eq!(i32::from(UserStatus::Other), 99);
433
434        assert_eq!(UserStatus::try_from(1).unwrap(), UserStatus::LoggedIn);
435        assert_eq!(UserStatus::try_from(2).unwrap(), UserStatus::NotLoggedIn);
436        assert_eq!(
437            UserStatus::try_from(3).unwrap(),
438            UserStatus::UserNotRecognised
439        );
440        assert_eq!(
441            UserStatus::try_from(4).unwrap(),
442            UserStatus::PasswordIncorrect
443        );
444        assert_eq!(
445            UserStatus::try_from(5).unwrap(),
446            UserStatus::PasswordChanged
447        );
448        assert_eq!(UserStatus::try_from(99).unwrap(), UserStatus::Other);
449
450        assert!(UserStatus::try_from(50).is_err());
451    }
452}