use crate::errors::SwiftValidationError;
use crate::fields::*;
use crate::parser::utils::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MT941 {
#[serde(rename = "20")]
pub field_20: Field20,
#[serde(rename = "21", skip_serializing_if = "Option::is_none")]
pub field_21: Option<Field21NoOption>,
#[serde(rename = "25")]
pub field_25: Field25AccountIdentification,
#[serde(rename = "28")]
pub field_28: Field28,
#[serde(rename = "13D", skip_serializing_if = "Option::is_none")]
pub field_13d: Option<Field13D>,
#[serde(rename = "60F", skip_serializing_if = "Option::is_none")]
pub field_60f: Option<Field60F>,
#[serde(rename = "90D", skip_serializing_if = "Option::is_none")]
pub field_90d: Option<Field90D>,
#[serde(rename = "90C", skip_serializing_if = "Option::is_none")]
pub field_90c: Option<Field90C>,
#[serde(rename = "62F")]
pub field_62f: Field62F,
#[serde(rename = "64", skip_serializing_if = "Option::is_none")]
pub field_64: Option<Field64>,
#[serde(rename = "65", skip_serializing_if = "Option::is_none")]
pub field_65: Option<Vec<Field65>>,
#[serde(rename = "86", skip_serializing_if = "Option::is_none")]
pub field_86: Option<Field86>,
}
impl MT941 {
pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
let mut parser = crate::parser::MessageParser::new(block4, "941");
let field_20 = parser.parse_field::<Field20>("20")?;
let field_21 = parser.parse_optional_field::<Field21NoOption>("21")?;
let field_25 = parser.parse_field::<Field25AccountIdentification>("25")?;
let field_28 = parser.parse_field::<Field28>("28")?;
let field_13d = parser.parse_optional_field::<Field13D>("13D")?;
let field_60f = parser.parse_optional_field::<Field60F>("60F")?;
let field_90d = parser.parse_optional_field::<Field90D>("90D")?;
let field_90c = parser.parse_optional_field::<Field90C>("90C")?;
let field_62f = parser.parse_field::<Field62F>("62F")?;
let field_64 = parser.parse_optional_field::<Field64>("64")?;
let mut field_65_vec = Vec::new();
while parser.detect_field("65") {
if let Ok(field) = parser.parse_field::<Field65>("65") {
field_65_vec.push(field);
} else {
break;
}
}
let field_65 = if field_65_vec.is_empty() {
None
} else {
Some(field_65_vec)
};
let field_86 = parser.parse_optional_field::<Field86>("86")?;
Ok(MT941 {
field_20,
field_21,
field_25,
field_28,
field_13d,
field_60f,
field_90d,
field_90c,
field_62f,
field_64,
field_65,
field_86,
})
}
fn get_base_currency(&self) -> &str {
&self.field_62f.currency[0..2]
}
fn validate_c1_currency_consistency(
&self,
stop_on_first_error: bool,
) -> Vec<SwiftValidationError> {
let mut errors = Vec::new();
let base_currency = self.get_base_currency();
if let Some(ref field_60f) = self.field_60f
&& &field_60f.currency[0..2] != base_currency
{
errors.push(SwiftValidationError::content_error(
"C27",
"60F",
&field_60f.currency,
&format!(
"Currency code in field 60F ({}) must have the same first two characters as field 62F ({})",
&field_60f.currency[0..2],
base_currency
),
"The first two characters of the three character currency code in fields 60F, 90D, 90C, 62F, 64 and 65 must be the same for all occurrences of these fields",
));
if stop_on_first_error {
return errors;
}
}
if let Some(ref field_90d) = self.field_90d
&& &field_90d.currency[0..2] != base_currency
{
errors.push(SwiftValidationError::content_error(
"C27",
"90D",
&field_90d.currency,
&format!(
"Currency code in field 90D ({}) must have the same first two characters as field 62F ({})",
&field_90d.currency[0..2],
base_currency
),
"The first two characters of the three character currency code in fields 60F, 90D, 90C, 62F, 64 and 65 must be the same for all occurrences of these fields",
));
if stop_on_first_error {
return errors;
}
}
if let Some(ref field_90c) = self.field_90c
&& &field_90c.currency[0..2] != base_currency
{
errors.push(SwiftValidationError::content_error(
"C27",
"90C",
&field_90c.currency,
&format!(
"Currency code in field 90C ({}) must have the same first two characters as field 62F ({})",
&field_90c.currency[0..2],
base_currency
),
"The first two characters of the three character currency code in fields 60F, 90D, 90C, 62F, 64 and 65 must be the same for all occurrences of these fields",
));
if stop_on_first_error {
return errors;
}
}
if let Some(ref field_64) = self.field_64
&& &field_64.currency[0..2] != base_currency
{
errors.push(SwiftValidationError::content_error(
"C27",
"64",
&field_64.currency,
&format!(
"Currency code in field 64 ({}) must have the same first two characters as field 62F ({})",
&field_64.currency[0..2],
base_currency
),
"The first two characters of the three character currency code in fields 60F, 90D, 90C, 62F, 64 and 65 must be the same for all occurrences of these fields",
));
if stop_on_first_error {
return errors;
}
}
if let Some(ref field_65_vec) = self.field_65 {
for (idx, field_65) in field_65_vec.iter().enumerate() {
if &field_65.currency[0..2] != base_currency {
errors.push(SwiftValidationError::content_error(
"C27",
"65",
&field_65.currency,
&format!(
"Currency code in field 65[{}] ({}) must have the same first two characters as field 62F ({})",
idx,
&field_65.currency[0..2],
base_currency
),
"The first two characters of the three character currency code in fields 60F, 90D, 90C, 62F, 64 and 65 must be the same for all occurrences of these fields",
));
if stop_on_first_error {
return errors;
}
}
}
}
errors
}
pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
let mut all_errors = Vec::new();
let c1_errors = self.validate_c1_currency_consistency(stop_on_first_error);
all_errors.extend(c1_errors);
if stop_on_first_error && !all_errors.is_empty() {
return all_errors;
}
all_errors
}
}
impl crate::traits::SwiftMessageBody for MT941 {
fn message_type() -> &'static str {
"941"
}
fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
Self::parse_from_block4(block4)
}
fn to_mt_string(&self) -> String {
let mut result = String::new();
append_field(&mut result, &self.field_20);
append_optional_field(&mut result, &self.field_21);
append_field(&mut result, &self.field_25);
append_field(&mut result, &self.field_28);
append_optional_field(&mut result, &self.field_13d);
append_optional_field(&mut result, &self.field_60f);
append_optional_field(&mut result, &self.field_90d);
append_optional_field(&mut result, &self.field_90c);
append_field(&mut result, &self.field_62f);
append_optional_field(&mut result, &self.field_64);
append_vec_field(&mut result, &self.field_65);
append_optional_field(&mut result, &self.field_86);
finalize_mt_string(result, false)
}
fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
MT941::validate_network_rules(self, stop_on_first_error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::SwiftField;
#[test]
fn test_mt941_validate_c1_currency_consistency_pass() {
let mt941 = MT941 {
field_20: Field20::parse("BALREP001").unwrap(),
field_21: None,
field_25: Field25AccountIdentification::parse("1234567890").unwrap(),
field_28: Field28::parse("1").unwrap(),
field_13d: None,
field_60f: Some(Field60F::parse("C251003EUR595771,95").unwrap()),
field_90d: Some(Field90D::parse("72EUR385920,").unwrap()),
field_90c: Some(Field90C::parse("44EUR450000,").unwrap()),
field_62f: Field62F::parse("C251003EUR659851,95").unwrap(),
field_64: Some(Field64::parse("C251003EUR480525,87").unwrap()),
field_65: Some(vec![Field65::parse("C251004EUR530691,95").unwrap()]),
field_86: None,
};
let errors = mt941.validate_network_rules(false);
assert!(
errors.is_empty(),
"Expected no validation errors, got: {:?}",
errors
);
}
#[test]
fn test_mt941_validate_c1_currency_consistency_fail_60f() {
let mt941 = MT941 {
field_20: Field20::parse("BALREP001").unwrap(),
field_21: None,
field_25: Field25AccountIdentification::parse("1234567890").unwrap(),
field_28: Field28::parse("1").unwrap(),
field_13d: None,
field_60f: Some(Field60F::parse("C251003USD595771,95").unwrap()), field_90d: None,
field_90c: None,
field_62f: Field62F::parse("C251003EUR659851,95").unwrap(),
field_64: None,
field_65: None,
field_86: None,
};
let errors = mt941.validate_network_rules(false);
assert_eq!(errors.len(), 1);
assert!(errors[0].message().contains("60F"));
assert!(errors[0].message().contains("US"));
assert!(errors[0].message().contains("EU"));
}
#[test]
fn test_mt941_validate_c1_currency_consistency_fail_multiple() {
let mt941 = MT941 {
field_20: Field20::parse("BALREP001").unwrap(),
field_21: None,
field_25: Field25AccountIdentification::parse("1234567890").unwrap(),
field_28: Field28::parse("1").unwrap(),
field_13d: None,
field_60f: Some(Field60F::parse("C251003USD595771,95").unwrap()), field_90d: Some(Field90D::parse("72GBP385920,").unwrap()), field_90c: Some(Field90C::parse("44JPY450000,").unwrap()), field_62f: Field62F::parse("C251003EUR659851,95").unwrap(), field_64: Some(Field64::parse("C251003CHF480525,87").unwrap()), field_65: Some(vec![
Field65::parse("C251004AUD530691,95").unwrap(), Field65::parse("C251005CAD530691,95").unwrap(), ]),
field_86: None,
};
let errors = mt941.validate_network_rules(false);
assert_eq!(errors.len(), 6); }
#[test]
fn test_mt941_validate_c1_stop_on_first_error() {
let mt941 = MT941 {
field_20: Field20::parse("BALREP001").unwrap(),
field_21: None,
field_25: Field25AccountIdentification::parse("1234567890").unwrap(),
field_28: Field28::parse("1").unwrap(),
field_13d: None,
field_60f: Some(Field60F::parse("C251003USD595771,95").unwrap()),
field_90d: Some(Field90D::parse("72GBP385920,").unwrap()),
field_90c: None,
field_62f: Field62F::parse("C251003EUR659851,95").unwrap(),
field_64: None,
field_65: None,
field_86: None,
};
let errors = mt941.validate_network_rules(true); assert_eq!(errors.len(), 1); }
}