swift_mt_message/messages/
mt950.rs

1use crate::errors::SwiftValidationError;
2use crate::fields::*;
3use crate::parser::utils::*;
4use serde::{Deserialize, Serialize};
5
6/// **MT950: Statement Message**
7///
8/// Detailed account statement for financial institution accounts.
9///
10/// **Usage:** Nostro/vostro statements, interbank reconciliation
11/// **Category:** Category 9 (Cash Management & Customer Status)
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct MT950 {
14    /// Transaction Reference Number (Field 20)
15    #[serde(rename = "20")]
16    pub field_20: Field20,
17
18    /// Account Identification (Field 25)
19    #[serde(rename = "25")]
20    pub field_25: Field25NoOption,
21
22    /// Statement Number/Sequence Number (Field 28C)
23    #[serde(rename = "28C")]
24    pub field_28c: Field28C,
25
26    /// Opening Balance (Field 60)
27    #[serde(rename = "60")]
28    pub field_60: Field60,
29
30    /// Statement Lines (Field 61)
31    #[serde(rename = "61", skip_serializing_if = "Option::is_none")]
32    pub field_61: Option<Vec<Field61>>,
33
34    /// Closing Balance (Field 62)
35    #[serde(rename = "62")]
36    pub field_62: Field62,
37
38    /// Closing Available Balance (Field 64)
39    #[serde(rename = "64", skip_serializing_if = "Option::is_none")]
40    pub field_64: Option<Field64>,
41}
42
43impl MT950 {
44    /// Parse message from Block 4 content
45    pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
46        let mut parser = crate::parser::MessageParser::new(block4, "950");
47
48        // Parse mandatory fields
49        let field_20 = parser.parse_field::<Field20>("20")?;
50        let field_25 = parser.parse_field::<Field25NoOption>("25")?;
51        let field_28c = parser.parse_field::<Field28C>("28C")?;
52
53        // Parse Field60 - check for both 60F and 60M variants
54        let field_60 = if parser.detect_field("60F") {
55            Field60::F(parser.parse_field::<Field60F>("60F")?)
56        } else if parser.detect_field("60M") {
57            Field60::M(parser.parse_field::<Field60M>("60M")?)
58        } else if parser.detect_field("60") {
59            // Try to parse as generic Field60
60            parser.parse_field::<Field60>("60")?
61        } else {
62            return Err(crate::errors::ParseError::InvalidFormat {
63                message: "MT950: Missing required field 60 (opening balance)".to_string(),
64            });
65        };
66
67        // Enable duplicate field handling for statement lines
68        parser = parser.with_duplicates(true);
69
70        // Parse optional statement lines (repetitive)
71        let mut field_61_vec = Vec::new();
72        while parser.detect_field("61") {
73            if let Ok(field) = parser.parse_field::<Field61>("61") {
74                field_61_vec.push(field);
75            } else {
76                break;
77            }
78        }
79        let field_61 = if field_61_vec.is_empty() {
80            None
81        } else {
82            Some(field_61_vec)
83        };
84
85        // Parse mandatory closing balance - check for both 62F and 62M variants
86        let field_62 = if parser.detect_field("62F") {
87            Field62::F(parser.parse_field::<Field62F>("62F")?)
88        } else if parser.detect_field("62M") {
89            Field62::M(parser.parse_field::<Field62M>("62M")?)
90        } else if parser.detect_field("62") {
91            // Try to parse as generic Field62
92            parser.parse_field::<Field62>("62")?
93        } else {
94            return Err(crate::errors::ParseError::InvalidFormat {
95                message: "MT950: Missing required field 62 (closing balance)".to_string(),
96            });
97        };
98
99        // Parse optional available balance
100        let field_64 = parser.parse_optional_field::<Field64>("64")?;
101
102        Ok(MT950 {
103            field_20,
104            field_25,
105            field_28c,
106            field_60,
107            field_61,
108            field_62,
109            field_64,
110        })
111    }
112
113    // ========================================================================
114    // NETWORK VALIDATION RULES (SR 2025 MT950)
115    // ========================================================================
116
117    // ========================================================================
118    // HELPER METHODS
119    // ========================================================================
120
121    /// Get the first two characters of currency code from field 60
122    fn get_field_60_currency_prefix(&self) -> &str {
123        match &self.field_60 {
124            Field60::F(field) => &field.currency[0..2],
125            Field60::M(field) => &field.currency[0..2],
126        }
127    }
128
129    /// Get the first two characters of currency code from field 62
130    fn get_field_62_currency_prefix(&self) -> &str {
131        match &self.field_62 {
132            Field62::F(field) => &field.currency[0..2],
133            Field62::M(field) => &field.currency[0..2],
134        }
135    }
136
137    /// Get the full currency code from field 60 for error messages
138    fn get_field_60_currency(&self) -> &str {
139        match &self.field_60 {
140            Field60::F(field) => &field.currency,
141            Field60::M(field) => &field.currency,
142        }
143    }
144
145    /// Get the full currency code from field 62 for error messages
146    fn get_field_62_currency(&self) -> &str {
147        match &self.field_62 {
148            Field62::F(field) => &field.currency,
149            Field62::M(field) => &field.currency,
150        }
151    }
152
153    // ========================================================================
154    // VALIDATION RULES
155    // ========================================================================
156
157    /// C1: Currency Code Consistency (Error code: C27)
158    /// The first two characters of the three character currency code in fields 60a, 62a and 64 must be the same
159    fn validate_c1_currency_consistency(&self) -> Vec<SwiftValidationError> {
160        let mut errors = Vec::new();
161
162        // Get base currency from field 60 (mandatory)
163        let base_currency_prefix = self.get_field_60_currency_prefix();
164        let base_currency = self.get_field_60_currency();
165
166        // Check field 62 (mandatory)
167        let field_62_prefix = self.get_field_62_currency_prefix();
168        let field_62_currency = self.get_field_62_currency();
169
170        if field_62_prefix != base_currency_prefix {
171            errors.push(SwiftValidationError::business_error(
172                "C27",
173                "62a",
174                vec!["60a".to_string()],
175                &format!(
176                    "Currency code mismatch: field 62a currency '{}' (prefix '{}') must have the same first two characters as field 60a currency '{}' (prefix '{}')",
177                    field_62_currency, field_62_prefix, base_currency, base_currency_prefix
178                ),
179                "The first two characters of the three character currency code in fields 60a, 62a and 64 must be the same",
180            ));
181        }
182
183        // Check field 64 if present (optional)
184        if let Some(ref field_64) = self.field_64 {
185            let field_64_prefix = &field_64.currency[0..2];
186            if field_64_prefix != base_currency_prefix {
187                errors.push(SwiftValidationError::business_error(
188                    "C27",
189                    "64",
190                    vec!["60a".to_string(), "62a".to_string()],
191                    &format!(
192                        "Currency code mismatch: field 64 currency '{}' (prefix '{}') must have the same first two characters as field 60a currency '{}' (prefix '{}')",
193                        field_64.currency, field_64_prefix, base_currency, base_currency_prefix
194                    ),
195                    "The first two characters of the three character currency code in fields 60a, 62a and 64 must be the same",
196                ));
197            }
198        }
199
200        errors
201    }
202
203    /// Main validation method - validates all network rules
204    /// Returns array of validation errors, respects stop_on_first_error flag
205    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
206        let mut all_errors = Vec::new();
207
208        // C1: Currency Code Consistency
209        let c1_errors = self.validate_c1_currency_consistency();
210        all_errors.extend(c1_errors);
211        if stop_on_first_error && !all_errors.is_empty() {
212            return all_errors;
213        }
214
215        all_errors
216    }
217}
218
219// Implement the SwiftMessageBody trait for MT950
220impl crate::traits::SwiftMessageBody for MT950 {
221    fn message_type() -> &'static str {
222        "950"
223    }
224
225    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
226        Self::parse_from_block4(block4)
227    }
228
229    fn to_mt_string(&self) -> String {
230        let mut result = String::new();
231
232        append_field(&mut result, &self.field_20);
233        append_field(&mut result, &self.field_25);
234        append_field(&mut result, &self.field_28c);
235        append_field(&mut result, &self.field_60);
236        append_vec_field(&mut result, &self.field_61);
237        append_field(&mut result, &self.field_62);
238        append_optional_field(&mut result, &self.field_64);
239
240        finalize_mt_string(result, false)
241    }
242
243    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
244        // Call the existing public method implementation
245        MT950::validate_network_rules(self, stop_on_first_error)
246    }
247}