use crate::errors::SwiftValidationError;
use crate::fields::{field34::Field34F, *};
use crate::parser::utils::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MT920 {
#[serde(rename = "20")]
pub field_20: Field20,
#[serde(rename = "#")]
pub sequence: Vec<MT920Sequence>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MT920Sequence {
#[serde(rename = "12")]
pub field_12: Field12,
#[serde(rename = "25")]
pub field_25: Field25,
#[serde(rename = "34F_1")]
pub floor_limit_debit: Option<Field34F>,
#[serde(rename = "34F_2")]
pub floor_limit_credit: Option<Field34F>,
}
impl MT920 {
pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
let mut parser = crate::parser::MessageParser::new(block4, "920");
let field_20 = parser.parse_field::<Field20>("20")?;
let mut sequence = Vec::new();
parser = parser.with_duplicates(true);
while parser.detect_field("12") {
let field_12 = parser.parse_field::<Field12>("12")?;
let field_25 = parser.parse_field::<Field25>("25")?;
let floor_limit_debit = parser.parse_optional_field::<Field34F>("34F")?;
let floor_limit_credit = parser.parse_optional_field::<Field34F>("34F")?;
if sequence.len() >= 100 {
return Err(crate::errors::ParseError::InvalidFormat {
message: "Maximum 100 repetitions allowed".to_string(),
});
}
sequence.push(MT920Sequence {
field_12,
field_25,
floor_limit_debit,
floor_limit_credit,
});
}
if sequence.is_empty() {
return Err(crate::errors::ParseError::InvalidFormat {
message: "At least one sequence is required in MT920".to_string(),
});
}
verify_parser_complete(&parser)?;
Ok(Self { field_20, sequence })
}
pub fn parse(input: &str) -> Result<Self, crate::errors::ParseError> {
let block4 = extract_block4(input)?;
Self::parse_from_block4(&block4)
}
pub fn to_mt_string(&self) -> String {
let mut result = String::new();
append_field(&mut result, &self.field_20);
for seq in &self.sequence {
append_field(&mut result, &seq.field_12);
append_field(&mut result, &seq.field_25);
append_optional_field(&mut result, &seq.floor_limit_debit);
append_optional_field(&mut result, &seq.floor_limit_credit);
}
finalize_mt_string(result, false)
}
const VALID_MESSAGE_TYPES: &'static [&'static str] = &["940", "941", "942", "950"];
fn validate_t88_message_type(&self) -> Vec<SwiftValidationError> {
let mut errors = Vec::new();
for (idx, seq) in self.sequence.iter().enumerate() {
let type_code = &seq.field_12.type_code;
if !Self::VALID_MESSAGE_TYPES.contains(&type_code.as_str()) {
errors.push(SwiftValidationError::format_error(
"T88",
"12",
type_code,
&format!("One of: {}", Self::VALID_MESSAGE_TYPES.join(", ")),
&format!(
"Sequence {}: Field 12 message type '{}' is not valid. Valid types: {}",
idx + 1,
type_code,
Self::VALID_MESSAGE_TYPES.join(", ")
),
));
}
}
errors
}
fn validate_c1_field_34f_requirement(&self) -> Vec<SwiftValidationError> {
let mut errors = Vec::new();
for (idx, seq) in self.sequence.iter().enumerate() {
if seq.field_12.type_code == "942" && seq.floor_limit_debit.is_none() {
errors.push(SwiftValidationError::business_error(
"C22",
"34F",
vec!["12".to_string()],
&format!(
"Sequence {}: Field 34F (Debit/Debit and Credit Floor Limit) is mandatory when field 12 contains '942'",
idx + 1
),
"When requesting MT 942 (Interim Transaction Report), at least the debit floor limit must be specified",
));
}
}
errors
}
fn validate_c2_dc_mark_usage(&self) -> Vec<SwiftValidationError> {
let mut errors = Vec::new();
for (idx, seq) in self.sequence.iter().enumerate() {
let has_debit = seq.floor_limit_debit.is_some();
let has_credit = seq.floor_limit_credit.is_some();
if has_debit && !has_credit {
if let Some(ref debit_field) = seq.floor_limit_debit
&& debit_field.indicator.is_some()
{
errors.push(SwiftValidationError::business_error(
"C23",
"34F",
vec![],
&format!(
"Sequence {}: D/C Mark must not be used when only one field 34F is present",
idx + 1
),
"When only one field 34F is present, the floor limit applies to both debit and credit amounts, and the D/C Mark subfield must not be used",
));
}
} else if has_debit && has_credit {
let debit_field = seq.floor_limit_debit.as_ref().unwrap();
let credit_field = seq.floor_limit_credit.as_ref().unwrap();
if debit_field.indicator != Some('D') {
errors.push(SwiftValidationError::business_error(
"C23",
"34F",
vec![],
&format!(
"Sequence {}: First field 34F must contain D/C Mark 'D' when both floor limits are present",
idx + 1
),
"When both fields 34F are present, the first field (debit floor limit) must contain D/C Mark = 'D'",
));
}
if credit_field.indicator != Some('C') {
errors.push(SwiftValidationError::business_error(
"C23",
"34F",
vec![],
&format!(
"Sequence {}: Second field 34F must contain D/C Mark 'C' when both floor limits are present",
idx + 1
),
"When both fields 34F are present, the second field (credit floor limit) must contain D/C Mark = 'C'",
));
}
}
}
errors
}
fn validate_c3_currency_consistency(&self) -> Vec<SwiftValidationError> {
let mut errors = Vec::new();
for (idx, seq) in self.sequence.iter().enumerate() {
if let (Some(debit_field), Some(credit_field)) =
(&seq.floor_limit_debit, &seq.floor_limit_credit)
&& debit_field.currency != credit_field.currency
{
errors.push(SwiftValidationError::business_error(
"C40",
"34F",
vec![],
&format!(
"Sequence {}: Currency code must be the same for all field 34F occurrences. Found '{}' and '{}'",
idx + 1,
debit_field.currency,
credit_field.currency
),
"Within each repetitive sequence, the currency code must be the same for each occurrence of field 34F",
));
}
}
errors
}
pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
let mut all_errors = Vec::new();
let t88_errors = self.validate_t88_message_type();
all_errors.extend(t88_errors);
if stop_on_first_error && !all_errors.is_empty() {
return all_errors;
}
let c1_errors = self.validate_c1_field_34f_requirement();
all_errors.extend(c1_errors);
if stop_on_first_error && !all_errors.is_empty() {
return all_errors;
}
let c2_errors = self.validate_c2_dc_mark_usage();
all_errors.extend(c2_errors);
if stop_on_first_error && !all_errors.is_empty() {
return all_errors;
}
let c3_errors = self.validate_c3_currency_consistency();
all_errors.extend(c3_errors);
all_errors
}
}
impl crate::traits::SwiftMessageBody for MT920 {
fn message_type() -> &'static str {
"920"
}
fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
MT920::parse_from_block4(block4)
}
fn to_mt_string(&self) -> String {
MT920::to_mt_string(self)
}
fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
MT920::validate_network_rules(self, stop_on_first_error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mt920_parse() {
let mt920_text = r#":20:REQ123456
:12:100
:25:/GB12ABCD12345678901234
:12:200
:25:/US98EFGH98765432109876
-"#;
let result = MT920::parse_from_block4(mt920_text);
if let Err(ref e) = result {
eprintln!("MT920 parse error: {:?}", e);
}
assert!(result.is_ok());
let mt920 = result.unwrap();
assert_eq!(mt920.field_20.reference, "REQ123456");
assert_eq!(mt920.sequence.len(), 2);
assert_eq!(mt920.sequence[0].field_12.type_code, "100");
assert_eq!(mt920.sequence[1].field_12.type_code, "200");
}
#[test]
fn test_mt920_validation() {
let mt920_text = r#":20:REQ123456
-"#;
let result = MT920::parse(mt920_text);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("At least one sequence")
);
}
#[test]
fn test_mt920_json_deserialization() {
let json = r##"{
"20": {
"reference": "REQ123456"
},
"#": [
{
"12": {
"type_code": "940"
},
"25": {
"authorisation": "1234567890"
}
}
]
}"##;
let result = serde_json::from_str::<MT920>(json);
if let Err(ref e) = result {
eprintln!("MT920 JSON deserialization error: {}", e);
}
assert!(result.is_ok(), "Failed to deserialize MT920 from JSON");
let mt920 = result.unwrap();
assert_eq!(mt920.field_20.reference, "REQ123456");
assert_eq!(mt920.sequence.len(), 1);
assert_eq!(mt920.sequence[0].field_12.type_code, "940");
assert_eq!(mt920.sequence[0].field_25.authorisation, "1234567890");
}
#[test]
fn test_mt920_swift_message_json() {
use crate::swift_message::SwiftMessage;
let json = r##"{
"basic_header": {
"application_id": "F",
"service_id": "01",
"sender_bic": "DEUTDEFF",
"logical_terminal": "DEUTDEFFXXXX",
"session_number": "0001",
"sequence_number": "000123"
},
"application_header": {
"direction": "I",
"message_type": "920",
"receiver_bic": "DEUTDEFF",
"destination_address": "DEUTDEFFXXXX",
"priority": "N"
},
"message_type": "920",
"fields": {
"20": {
"reference": "REQ123456"
},
"#": [
{
"12": {
"type_code": "940"
},
"25": {
"authorisation": "1234567890"
}
}
]
}
}"##;
let result = serde_json::from_str::<SwiftMessage<MT920>>(json);
if let Err(ref e) = result {
eprintln!("SwiftMessage<MT920> JSON deserialization error: {}", e);
}
assert!(
result.is_ok(),
"Failed to deserialize SwiftMessage<MT920> from JSON"
);
let swift_msg = result.unwrap();
assert_eq!(swift_msg.message_type, "920");
assert_eq!(swift_msg.fields.field_20.reference, "REQ123456");
assert_eq!(swift_msg.fields.sequence.len(), 1);
}
}