use crate::error::Result;
use crate::message::MessageBuilder;
use crate::model::message::FixMessage;
use crate::model::types::MsgType;
use chrono::Utc;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Heartbeat {
pub test_req_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BusinessRejectReason {
Other = 0,
UnknownId = 1,
UnknownSecurity = 2,
UnsupportedMessageType = 3,
ApplicationNotAvailable = 4,
ConditionallyRequiredFieldMissing = 5,
NotAuthorized = 6,
DeliverToFirmNotAvailableAtThisTime = 7,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusinessMessageReject {
pub ref_msg_type: String,
pub business_reject_reason: BusinessRejectReason,
pub business_reject_ref_id: Option<String>,
pub text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestRequest {
pub test_req_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResendRequest {
pub begin_seq_no: u32,
pub end_seq_no: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SequenceReset {
pub new_seq_no: u32,
pub gap_fill_flag: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Reject {
pub ref_seq_num: u32,
pub ref_tag_id: Option<u32>,
pub ref_msg_type: Option<String>,
pub session_reject_reason: Option<u32>,
pub text: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SessionRejectReason {
InvalidTagNumber = 0,
RequiredTagMissing = 1,
TagNotDefinedForMessageType = 2,
UndefinedTag = 3,
TagSpecifiedWithoutValue = 4,
ValueIncorrectForTag = 5,
IncorrectDataFormat = 6,
DecryptionProblem = 7,
SignatureProblem = 8,
CompIdProblem = 9,
SendingTimeAccuracyProblem = 10,
InvalidMsgType = 11,
XmlValidationError = 12,
TagAppearsMoreThanOnce = 13,
TagSpecifiedOutOfOrder = 14,
RepeatingGroupFieldsOutOfOrder = 15,
IncorrectNumInGroupCount = 16,
NonDataValueIncludesFieldDelimiter = 17,
Other = 99,
}
impl_json_display!(Heartbeat);
impl_json_display!(TestRequest);
impl_json_display!(ResendRequest);
impl_json_display!(SequenceReset);
impl_json_display!(Reject);
impl_json_display!(BusinessMessageReject);
impl Heartbeat {
pub fn new() -> Self {
Self { test_req_id: None }
}
pub fn new_response(test_req_id: String) -> Self {
Self {
test_req_id: Some(test_req_id),
}
}
pub fn is_test_response(&self) -> bool {
self.test_req_id.is_some()
}
pub fn to_fix_message(
&self,
sender_comp_id: String,
target_comp_id: String,
msg_seq_num: u32,
) -> Result<FixMessage> {
let mut builder = MessageBuilder::new()
.msg_type(MsgType::Heartbeat)
.sender_comp_id(sender_comp_id)
.target_comp_id(target_comp_id)
.msg_seq_num(msg_seq_num)
.sending_time(Utc::now());
if let Some(ref test_req_id) = self.test_req_id {
builder = builder.field(112, test_req_id.clone());
}
builder.build()
}
}
impl TestRequest {
pub fn new(test_req_id: String) -> Self {
Self { test_req_id }
}
pub fn new_with_timestamp() -> Self {
let test_req_id = format!("TESTREQ_{}", Utc::now().timestamp_millis());
Self::new(test_req_id)
}
pub fn to_fix_message(
&self,
sender_comp_id: String,
target_comp_id: String,
msg_seq_num: u32,
) -> Result<FixMessage> {
MessageBuilder::new()
.msg_type(MsgType::TestRequest)
.sender_comp_id(sender_comp_id)
.target_comp_id(target_comp_id)
.msg_seq_num(msg_seq_num)
.sending_time(Utc::now())
.field(112, self.test_req_id.clone()) .build()
}
}
impl ResendRequest {
pub fn new(begin_seq_no: u32, end_seq_no: u32) -> Self {
Self {
begin_seq_no,
end_seq_no,
}
}
pub fn new_from_sequence(begin_seq_no: u32) -> Self {
Self {
begin_seq_no,
end_seq_no: 0, }
}
pub fn is_infinite_range(&self) -> bool {
self.end_seq_no == 0
}
pub fn message_count(&self) -> Option<u32> {
if self.is_infinite_range() {
None
} else {
Some(self.end_seq_no.saturating_sub(self.begin_seq_no) + 1)
}
}
pub fn to_fix_message(
&self,
sender_comp_id: String,
target_comp_id: String,
msg_seq_num: u32,
) -> Result<FixMessage> {
MessageBuilder::new()
.msg_type(MsgType::ResendRequest)
.sender_comp_id(sender_comp_id)
.target_comp_id(target_comp_id)
.msg_seq_num(msg_seq_num)
.sending_time(Utc::now())
.field(7, self.begin_seq_no.to_string()) .field(16, self.end_seq_no.to_string()) .build()
}
}
impl SequenceReset {
pub fn new(new_seq_no: u32) -> Self {
Self {
new_seq_no,
gap_fill_flag: None,
}
}
pub fn new_gap_fill(new_seq_no: u32) -> Self {
Self {
new_seq_no,
gap_fill_flag: Some(true),
}
}
pub fn new_reset(new_seq_no: u32) -> Self {
Self {
new_seq_no,
gap_fill_flag: Some(false),
}
}
pub fn is_gap_fill(&self) -> bool {
self.gap_fill_flag.unwrap_or(false)
}
pub fn to_fix_message(
&self,
sender_comp_id: String,
target_comp_id: String,
msg_seq_num: u32,
) -> Result<FixMessage> {
let mut builder = MessageBuilder::new()
.msg_type(MsgType::SequenceReset)
.sender_comp_id(sender_comp_id)
.target_comp_id(target_comp_id)
.msg_seq_num(msg_seq_num)
.sending_time(Utc::now())
.field(36, self.new_seq_no.to_string());
if let Some(gap_fill) = self.gap_fill_flag {
builder = builder.field(123, if gap_fill { "Y" } else { "N" }.to_string());
}
builder.build()
}
}
impl Reject {
pub fn new(ref_seq_num: u32) -> Self {
Self {
ref_seq_num,
ref_tag_id: None,
ref_msg_type: None,
session_reject_reason: None,
text: None,
}
}
pub fn new_detailed(
ref_seq_num: u32,
ref_tag_id: Option<u32>,
ref_msg_type: Option<String>,
session_reject_reason: Option<SessionRejectReason>,
text: Option<String>,
) -> Self {
Self {
ref_seq_num,
ref_tag_id,
ref_msg_type,
session_reject_reason: session_reject_reason.map(|r| r as u32),
text,
}
}
pub fn new_invalid_tag(ref_seq_num: u32, tag_id: u32) -> Self {
Self::new_detailed(
ref_seq_num,
Some(tag_id),
None,
Some(SessionRejectReason::InvalidTagNumber),
Some(format!("Invalid tag number: {tag_id}")),
)
}
pub fn new_missing_tag(ref_seq_num: u32, tag_id: u32, msg_type: String) -> Self {
Self::new_detailed(
ref_seq_num,
Some(tag_id),
Some(msg_type),
Some(SessionRejectReason::RequiredTagMissing),
Some(format!("Required tag {tag_id} missing")),
)
}
pub fn new_incorrect_format(ref_seq_num: u32, tag_id: u32, text: String) -> Self {
Self::new_detailed(
ref_seq_num,
Some(tag_id),
None,
Some(SessionRejectReason::IncorrectDataFormat),
Some(text),
)
}
pub fn to_fix_message(
&self,
sender_comp_id: String,
target_comp_id: String,
msg_seq_num: u32,
) -> Result<FixMessage> {
let mut builder = MessageBuilder::new()
.msg_type(MsgType::Reject)
.sender_comp_id(sender_comp_id)
.target_comp_id(target_comp_id)
.msg_seq_num(msg_seq_num)
.sending_time(Utc::now())
.field(45, self.ref_seq_num.to_string());
if let Some(ref_tag_id) = self.ref_tag_id {
builder = builder.field(371, ref_tag_id.to_string()); }
if let Some(ref ref_msg_type) = self.ref_msg_type {
builder = builder.field(372, ref_msg_type.clone()); }
if let Some(session_reject_reason) = self.session_reject_reason {
builder = builder.field(373, session_reject_reason.to_string()); }
if let Some(ref text) = self.text {
builder = builder.field(58, text.clone()); }
builder.build()
}
}
impl BusinessMessageReject {
pub fn new(ref_msg_type: String, business_reject_reason: BusinessRejectReason) -> Self {
Self {
ref_msg_type,
business_reject_reason,
business_reject_ref_id: None,
text: None,
}
}
pub fn with_ref_id(mut self, business_reject_ref_id: String) -> Self {
self.business_reject_ref_id = Some(business_reject_ref_id);
self
}
pub fn with_text(mut self, text: String) -> Self {
self.text = Some(text);
self
}
pub fn to_fix_message(
&self,
sender_comp_id: String,
target_comp_id: String,
msg_seq_num: u32,
) -> Result<FixMessage> {
let mut builder = MessageBuilder::new()
.msg_type(MsgType::BusinessMessageReject)
.sender_comp_id(sender_comp_id)
.target_comp_id(target_comp_id)
.msg_seq_num(msg_seq_num)
.sending_time(Utc::now())
.field(372, self.ref_msg_type.clone()) .field(380, (self.business_reject_reason as u32).to_string());
if let Some(ref ref_id) = self.business_reject_ref_id {
builder = builder.field(379, ref_id.clone()); }
if let Some(ref text) = self.text {
builder = builder.field(58, text.clone()); }
builder.build()
}
}
impl Default for Heartbeat {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_heartbeat_creation() {
let heartbeat = Heartbeat::new();
assert_eq!(heartbeat.test_req_id, None);
assert!(!heartbeat.is_test_response());
let response = Heartbeat::new_response("TEST123".to_string());
assert_eq!(response.test_req_id, Some("TEST123".to_string()));
assert!(response.is_test_response());
}
#[test]
fn test_test_request_creation() {
let test_req = TestRequest::new("REQ123".to_string());
assert_eq!(test_req.test_req_id, "REQ123");
let timestamp_req = TestRequest::new_with_timestamp();
assert!(timestamp_req.test_req_id.starts_with("TESTREQ_"));
}
#[test]
fn test_resend_request_creation() {
let resend = ResendRequest::new(10, 20);
assert_eq!(resend.begin_seq_no, 10);
assert_eq!(resend.end_seq_no, 20);
assert!(!resend.is_infinite_range());
assert_eq!(resend.message_count(), Some(11));
let infinite = ResendRequest::new_from_sequence(15);
assert_eq!(infinite.begin_seq_no, 15);
assert_eq!(infinite.end_seq_no, 0);
assert!(infinite.is_infinite_range());
assert_eq!(infinite.message_count(), None);
}
#[test]
fn test_reject_creation() {
let basic_reject = Reject::new(123);
assert_eq!(basic_reject.ref_seq_num, 123);
assert_eq!(basic_reject.ref_tag_id, None);
let invalid_tag = Reject::new_invalid_tag(456, 999);
assert_eq!(invalid_tag.ref_seq_num, 456);
assert_eq!(invalid_tag.ref_tag_id, Some(999));
assert_eq!(
invalid_tag.session_reject_reason,
Some(SessionRejectReason::InvalidTagNumber as u32)
);
let missing_tag = Reject::new_missing_tag(789, 35, "D".to_string());
assert_eq!(missing_tag.ref_seq_num, 789);
assert_eq!(missing_tag.ref_tag_id, Some(35));
assert_eq!(missing_tag.ref_msg_type, Some("D".to_string()));
assert_eq!(
missing_tag.session_reject_reason,
Some(SessionRejectReason::RequiredTagMissing as u32)
);
}
#[test]
fn test_session_reject_reason_values() {
assert_eq!(SessionRejectReason::InvalidTagNumber as u32, 0);
assert_eq!(SessionRejectReason::RequiredTagMissing as u32, 1);
assert_eq!(SessionRejectReason::Other as u32, 99);
}
#[test]
fn test_heartbeat_to_fix_message() {
let heartbeat = Heartbeat::new_response("TEST123".to_string());
let fix_msg = heartbeat.to_fix_message("SENDER".to_string(), "TARGET".to_string(), 100);
assert!(fix_msg.is_ok());
let msg = fix_msg.unwrap();
assert_eq!(msg.get_field(35), Some(&"0".to_string())); assert_eq!(msg.get_field(112), Some(&"TEST123".to_string())); }
#[test]
fn test_test_request_to_fix_message() {
let test_req = TestRequest::new("REQ456".to_string());
let fix_msg = test_req.to_fix_message("CLIENT".to_string(), "SERVER".to_string(), 200);
assert!(fix_msg.is_ok());
let msg = fix_msg.unwrap();
assert_eq!(msg.get_field(35), Some(&"1".to_string())); assert_eq!(msg.get_field(112), Some(&"REQ456".to_string())); }
#[test]
fn test_business_message_reject_to_fix_message() {
let bmr = BusinessMessageReject::new(
"D".to_string(),
BusinessRejectReason::UnsupportedMessageType,
)
.with_ref_id("ABC123".to_string())
.with_text("Unsupported type".to_string());
let fix_msg = bmr.to_fix_message("SENDER".to_string(), "TARGET".to_string(), 77);
assert!(fix_msg.is_ok());
let msg = fix_msg.unwrap();
assert_eq!(msg.get_field(35), Some(&"j".to_string())); assert_eq!(msg.get_field(372), Some(&"D".to_string())); assert_eq!(msg.get_field(379), Some(&"ABC123".to_string())); assert_eq!(
msg.get_field(380),
Some(&(BusinessRejectReason::UnsupportedMessageType as u32).to_string())
); assert_eq!(msg.get_field(58), Some(&"Unsupported type".to_string())); }
}