deribit_fix/message/user/
user_response.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 12/8/25
5******************************************************************************/
6
7//! User Response 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// Re-export UserStatus from user_request module
18pub use super::user_request::UserStatus;
19
20/// User Response message (MsgType = 'BF')
21#[derive(Clone, PartialEq, Serialize, Deserialize)]
22pub struct UserResponse {
23    /// User request ID
24    pub user_request_id: String,
25    /// Username
26    pub username: String,
27    /// User status
28    pub user_status: UserStatus,
29    /// User status text
30    pub user_status_text: Option<String>,
31    /// Raw data length
32    pub raw_data_length: Option<i32>,
33    /// Raw data
34    pub raw_data: Option<Vec<u8>>,
35    /// Custom label
36    pub deribit_label: Option<String>,
37}
38
39impl UserResponse {
40    /// Create a new user response
41    pub fn new(user_request_id: String, username: String, user_status: UserStatus) -> Self {
42        Self {
43            user_request_id,
44            username,
45            user_status,
46            user_status_text: None,
47            raw_data_length: None,
48            raw_data: None,
49            deribit_label: None,
50        }
51    }
52
53    /// Create a successful login response
54    pub fn logged_in(user_request_id: String, username: String) -> Self {
55        let mut response = Self::new(user_request_id, username, UserStatus::LoggedIn);
56        response.user_status_text = Some("User logged in successfully".to_string());
57        response
58    }
59
60    /// Create a successful logout response
61    pub fn logged_out(user_request_id: String, username: String) -> Self {
62        let mut response = Self::new(user_request_id, username, UserStatus::NotLoggedIn);
63        response.user_status_text = Some("User logged out successfully".to_string());
64        response
65    }
66
67    /// Create a password changed response
68    pub fn password_changed(user_request_id: String, username: String) -> Self {
69        let mut response = Self::new(user_request_id, username, UserStatus::PasswordChanged);
70        response.user_status_text = Some("Password changed successfully".to_string());
71        response
72    }
73
74    /// Create an error response for unrecognized user
75    pub fn user_not_recognised(user_request_id: String, username: String) -> Self {
76        let mut response = Self::new(user_request_id, username, UserStatus::UserNotRecognised);
77        response.user_status_text = Some("User not recognised".to_string());
78        response
79    }
80
81    /// Create an error response for incorrect password
82    pub fn password_incorrect(user_request_id: String, username: String) -> Self {
83        let mut response = Self::new(user_request_id, username, UserStatus::PasswordIncorrect);
84        response.user_status_text = Some("Password incorrect".to_string());
85        response
86    }
87
88    /// Create a generic error response
89    pub fn error(user_request_id: String, username: String, error_text: String) -> Self {
90        let mut response = Self::new(user_request_id, username, UserStatus::Other);
91        response.user_status_text = Some(error_text);
92        response
93    }
94
95    /// Set user status text
96    pub fn with_user_status_text(mut self, user_status_text: String) -> Self {
97        self.user_status_text = Some(user_status_text);
98        self
99    }
100
101    /// Set raw data
102    pub fn with_raw_data(mut self, raw_data: Vec<u8>) -> Self {
103        self.raw_data_length = Some(raw_data.len() as i32);
104        self.raw_data = Some(raw_data);
105        self
106    }
107
108    /// Set custom label
109    pub fn with_label(mut self, label: String) -> Self {
110        self.deribit_label = Some(label);
111        self
112    }
113
114    /// Convert to FIX message
115    pub fn to_fix_message(
116        &self,
117        sender_comp_id: &str,
118        target_comp_id: &str,
119        msg_seq_num: u32,
120    ) -> DeribitFixResult<String> {
121        let mut builder = MessageBuilder::new()
122            .msg_type(MsgType::UserResponse)
123            .sender_comp_id(sender_comp_id.to_string())
124            .target_comp_id(target_comp_id.to_string())
125            .msg_seq_num(msg_seq_num)
126            .sending_time(Utc::now());
127
128        // Required fields
129        builder = builder
130            .field(923, self.user_request_id.clone()) // UserRequestID
131            .field(553, self.username.clone()) // Username
132            .field(926, i32::from(self.user_status).to_string()); // UserStatus
133
134        // Optional fields
135        if let Some(user_status_text) = &self.user_status_text {
136            builder = builder.field(927, user_status_text.clone());
137        }
138
139        if let Some(raw_data_length) = &self.raw_data_length {
140            builder = builder.field(95, raw_data_length.to_string());
141        }
142
143        if let Some(raw_data) = &self.raw_data {
144            // Convert raw data to base64 for FIX transmission
145            let encoded_data = general_purpose::STANDARD.encode(raw_data);
146            builder = builder.field(96, encoded_data);
147        }
148
149        if let Some(deribit_label) = &self.deribit_label {
150            builder = builder.field(100010, deribit_label.clone());
151        }
152
153        Ok(builder.build()?.to_string())
154    }
155}
156
157impl_json_display!(UserResponse);
158impl_json_debug_pretty!(UserResponse);
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_user_response_creation() {
166        let response = UserResponse::new(
167            "UR123".to_string(),
168            "testuser".to_string(),
169            UserStatus::LoggedIn,
170        );
171
172        assert_eq!(response.user_request_id, "UR123");
173        assert_eq!(response.username, "testuser");
174        assert_eq!(response.user_status, UserStatus::LoggedIn);
175        assert!(response.user_status_text.is_none());
176        assert!(response.raw_data.is_none());
177    }
178
179    #[test]
180    fn test_user_response_logged_in() {
181        let response = UserResponse::logged_in("UR456".to_string(), "user1".to_string());
182
183        assert_eq!(response.user_status, UserStatus::LoggedIn);
184        assert_eq!(response.username, "user1");
185        assert_eq!(
186            response.user_status_text,
187            Some("User logged in successfully".to_string())
188        );
189    }
190
191    #[test]
192    fn test_user_response_logged_out() {
193        let response = UserResponse::logged_out("UR789".to_string(), "user2".to_string());
194
195        assert_eq!(response.user_status, UserStatus::NotLoggedIn);
196        assert_eq!(response.username, "user2");
197        assert_eq!(
198            response.user_status_text,
199            Some("User logged out successfully".to_string())
200        );
201    }
202
203    #[test]
204    fn test_user_response_password_changed() {
205        let response = UserResponse::password_changed("UR999".to_string(), "user3".to_string());
206
207        assert_eq!(response.user_status, UserStatus::PasswordChanged);
208        assert_eq!(response.username, "user3");
209        assert_eq!(
210            response.user_status_text,
211            Some("Password changed successfully".to_string())
212        );
213    }
214
215    #[test]
216    fn test_user_response_user_not_recognised() {
217        let response =
218            UserResponse::user_not_recognised("UR111".to_string(), "unknown_user".to_string());
219
220        assert_eq!(response.user_status, UserStatus::UserNotRecognised);
221        assert_eq!(response.username, "unknown_user");
222        assert_eq!(
223            response.user_status_text,
224            Some("User not recognised".to_string())
225        );
226    }
227
228    #[test]
229    fn test_user_response_password_incorrect() {
230        let response = UserResponse::password_incorrect("UR222".to_string(), "user4".to_string());
231
232        assert_eq!(response.user_status, UserStatus::PasswordIncorrect);
233        assert_eq!(response.username, "user4");
234        assert_eq!(
235            response.user_status_text,
236            Some("Password incorrect".to_string())
237        );
238    }
239
240    #[test]
241    fn test_user_response_error() {
242        let response = UserResponse::error(
243            "UR333".to_string(),
244            "user5".to_string(),
245            "System temporarily unavailable".to_string(),
246        );
247
248        assert_eq!(response.user_status, UserStatus::Other);
249        assert_eq!(response.username, "user5");
250        assert_eq!(
251            response.user_status_text,
252            Some("System temporarily unavailable".to_string())
253        );
254    }
255
256    #[test]
257    fn test_user_response_with_options() {
258        let raw_data = vec![10, 20, 30, 40];
259        let response = UserResponse::new(
260            "UR444".to_string(),
261            "user6".to_string(),
262            UserStatus::LoggedIn,
263        )
264        .with_user_status_text("Custom login message".to_string())
265        .with_raw_data(raw_data.clone())
266        .with_label("test-user-response".to_string());
267
268        assert_eq!(
269            response.user_status_text,
270            Some("Custom login message".to_string())
271        );
272        assert_eq!(response.raw_data, Some(raw_data));
273        assert_eq!(response.raw_data_length, Some(4));
274        assert_eq!(
275            response.deribit_label,
276            Some("test-user-response".to_string())
277        );
278    }
279
280    #[test]
281    fn test_user_response_to_fix_message() {
282        let response = UserResponse::logged_in("UR123".to_string(), "testuser".to_string())
283            .with_label("test-label".to_string());
284
285        let fix_message = response.to_fix_message("SENDER", "TARGET", 1).unwrap();
286
287        // Check that the message contains required fields
288        assert!(fix_message.contains("35=BF")); // MsgType
289        assert!(fix_message.contains("923=UR123")); // UserRequestID
290        assert!(fix_message.contains("553=testuser")); // Username
291        assert!(fix_message.contains("926=1")); // UserStatus=LoggedIn
292        assert!(fix_message.contains("927=User logged in successfully")); // UserStatusText
293        assert!(fix_message.contains("100010=test-label")); // Custom label
294    }
295
296    #[test]
297    fn test_user_response_minimal_fix_message() {
298        let response = UserResponse::new(
299            "UR456".to_string(),
300            "user".to_string(),
301            UserStatus::NotLoggedIn,
302        );
303
304        let fix_message = response.to_fix_message("SENDER", "TARGET", 2).unwrap();
305
306        // Check required fields only
307        assert!(fix_message.contains("35=BF")); // MsgType
308        assert!(fix_message.contains("923=UR456")); // UserRequestID
309        assert!(fix_message.contains("553=user")); // Username
310        assert!(fix_message.contains("926=2")); // UserStatus=NotLoggedIn
311
312        // Check optional fields are not present when not set
313        // Use SOH character (\x01) to be more precise and avoid false matches
314        assert!(!fix_message.contains("\x01927=")); // UserStatusText field not set
315        assert!(!fix_message.contains("\x0195=")); // RawDataLength field not set
316        assert!(!fix_message.contains("\x0196=")); // RawData field not set
317    }
318
319    #[test]
320    fn test_user_response_with_raw_data() {
321        let raw_data = vec![0xFF, 0xFE, 0xFD];
322        let response = UserResponse::new(
323            "UR789".to_string(),
324            "datauser".to_string(),
325            UserStatus::LoggedIn,
326        )
327        .with_raw_data(raw_data.clone());
328
329        let fix_message = response.to_fix_message("SENDER", "TARGET", 3).unwrap();
330
331        // Check that raw data fields are present
332        assert!(fix_message.contains("95=3")); // RawDataLength
333        assert!(fix_message.contains("96=")); // RawData field should be present (base64 encoded)
334    }
335}