Skip to main content

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