swift_mt_message/messages/
mt210.rs

1use crate::errors::{ParseError, SwiftValidationError};
2use crate::fields::*;
3use crate::parser::MessageParser;
4use crate::parser::utils::*;
5use serde::{Deserialize, Serialize};
6
7/// **MT210: Notice to Receive**
8///
9/// Advises correspondent that funds have been/will be deposited to account.
10///
11/// **Usage:** Deposit notifications, account funding notices
12/// **Category:** Category 2 (Financial Institution Transfers)
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct MT210 {
15    /// Transaction Reference Number (Field 20)
16    #[serde(rename = "20")]
17    pub transaction_reference: Field20,
18
19    /// Account Identification (Field 25)
20    #[serde(rename = "25", skip_serializing_if = "Option::is_none")]
21    pub account_identification: Option<Field25NoOption>,
22
23    /// Value Date (Field 30)
24    #[serde(rename = "30")]
25    pub value_date: Field30,
26
27    /// Transactions (repeatable)
28    #[serde(rename = "#", default)]
29    pub transactions: Vec<MT210Transaction>,
30}
31
32/// Individual transaction within MT210
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct MT210Transaction {
35    /// Related Reference (Field 21)
36    #[serde(rename = "21", skip_serializing_if = "Option::is_none")]
37    pub related_reference: Option<Field21NoOption>,
38
39    /// Currency Code, Amount (Field 32B)
40    #[serde(rename = "32B")]
41    pub currency_amount: Field32B,
42
43    /// Ordering Customer (Field 50)
44    #[serde(flatten, skip_serializing_if = "Option::is_none")]
45    pub ordering_customer: Option<Field50>,
46
47    /// Ordering Institution (Field 52)
48    #[serde(flatten, skip_serializing_if = "Option::is_none")]
49    pub ordering_institution: Option<Field52OrderingInstitution>,
50
51    /// Intermediary Institution (Field 56)
52    #[serde(flatten, skip_serializing_if = "Option::is_none")]
53    pub intermediary: Option<Field56>,
54}
55
56impl MT210 {
57    /// Parse MT210 from a raw SWIFT message string
58    pub fn parse_from_block4(block4: &str) -> Result<Self, ParseError> {
59        let mut parser = MessageParser::new(block4, "210");
60
61        // Parse mandatory fields
62        let transaction_reference = parser.parse_field::<Field20>("20")?;
63
64        // Parse optional Field 25
65        let account_identification = parser.parse_optional_field::<Field25NoOption>("25")?;
66
67        // Parse mandatory Field 30
68        let value_date = parser.parse_field::<Field30>("30")?;
69
70        // Parse repeating transaction sequences - enable duplicates mode
71        parser = parser.with_duplicates(true);
72        let mut transactions = Vec::new();
73
74        while parser.detect_field("21") || parser.detect_field("32B") {
75            // Parse optional Field 21 - Related Reference
76            let related_reference = parser.parse_optional_field::<Field21NoOption>("21")?;
77
78            // Parse mandatory Field 32B - Currency Code, Amount
79            let currency_amount = parser.parse_field::<Field32B>("32B")?;
80
81            // Parse optional Field 50 - Ordering Customer
82            let ordering_customer = parser.parse_optional_variant_field::<Field50>("50")?;
83
84            // Parse optional Field 52 - Ordering Institution
85            let ordering_institution =
86                parser.parse_optional_variant_field::<Field52OrderingInstitution>("52")?;
87
88            // Parse optional Field 56 - Intermediary Institution
89            let intermediary = parser.parse_optional_variant_field::<Field56>("56")?;
90
91            transactions.push(MT210Transaction {
92                related_reference,
93                currency_amount,
94                ordering_customer,
95                ordering_institution,
96                intermediary,
97            });
98
99            // Limit to 10 sequences
100            if transactions.len() >= 10 {
101                break;
102            }
103        }
104
105        Ok(MT210 {
106            transaction_reference,
107            account_identification,
108            value_date,
109            transactions,
110        })
111    }
112
113    // ========================================================================
114    // NETWORK VALIDATION RULES (SR 2025 MT210)
115    // ========================================================================
116
117    /// Maximum number of repetitive sequences allowed
118    const MAX_REPETITIVE_SEQUENCES: usize = 10;
119
120    // ========================================================================
121    // VALIDATION RULES (C1-C3)
122    // ========================================================================
123
124    /// C1: Repetitive Sequence Count (Error code: T10)
125    /// The repetitive sequence must not appear more than ten times
126    fn validate_c1_repetitive_sequence_count(&self) -> Option<SwiftValidationError> {
127        if self.transactions.len() > Self::MAX_REPETITIVE_SEQUENCES {
128            return Some(SwiftValidationError::format_error(
129                "T10",
130                "21",
131                &self.transactions.len().to_string(),
132                &format!("Max {} occurrences", Self::MAX_REPETITIVE_SEQUENCES),
133                &format!(
134                    "The repetitive sequence must not appear more than {} times. Found {} occurrences",
135                    Self::MAX_REPETITIVE_SEQUENCES,
136                    self.transactions.len()
137                ),
138            ));
139        }
140
141        None
142    }
143
144    /// C2: Ordering Customer and Ordering Institution Mutual Exclusivity (Error code: C06)
145    /// Either field 50a or field 52a, but not both, must be present in a repetitive sequence
146    fn validate_c2_mutual_exclusivity(&self) -> Vec<SwiftValidationError> {
147        let mut errors = Vec::new();
148
149        for (idx, transaction) in self.transactions.iter().enumerate() {
150            let has_ordering_customer = transaction.ordering_customer.is_some();
151            let has_ordering_institution = transaction.ordering_institution.is_some();
152
153            if has_ordering_customer && has_ordering_institution {
154                // Both present - NOT ALLOWED
155                errors.push(SwiftValidationError::content_error(
156                    "C06",
157                    "50a/52a",
158                    "",
159                    &format!(
160                        "Transaction {}: Either field 50a (Ordering Customer) or field 52a (Ordering Institution), but not both, must be present",
161                        idx + 1
162                    ),
163                    "Field 50a and field 52a are mutually exclusive. Only one may be present in each repetitive sequence",
164                ));
165            } else if !has_ordering_customer && !has_ordering_institution {
166                // Neither present - NOT ALLOWED
167                errors.push(SwiftValidationError::content_error(
168                    "C06",
169                    "50a/52a",
170                    "",
171                    &format!(
172                        "Transaction {}: Either field 50a (Ordering Customer) or field 52a (Ordering Institution) must be present",
173                        idx + 1
174                    ),
175                    "At least one of field 50a or field 52a must be present in each repetitive sequence",
176                ));
177            }
178        }
179
180        errors
181    }
182
183    /// C3: Currency Code Consistency (Error code: C02)
184    /// The currency code must be the same for all occurrences of field 32B
185    fn validate_c3_currency_consistency(&self) -> Option<SwiftValidationError> {
186        if self.transactions.is_empty() {
187            return None;
188        }
189
190        // Get the currency from the first transaction
191        let first_currency = &self.transactions[0].currency_amount.currency;
192
193        // Check if all transactions have the same currency
194        for (idx, transaction) in self.transactions.iter().enumerate().skip(1) {
195            if &transaction.currency_amount.currency != first_currency {
196                return Some(SwiftValidationError::content_error(
197                    "C02",
198                    "32B",
199                    &transaction.currency_amount.currency,
200                    &format!(
201                        "Transaction {}: Currency code in field 32B ({}) must be the same as in the first transaction ({})",
202                        idx + 1,
203                        transaction.currency_amount.currency,
204                        first_currency
205                    ),
206                    "The currency code must be the same for all occurrences of field 32B in the message",
207                ));
208            }
209        }
210
211        None
212    }
213
214    /// Main validation method - validates all network rules
215    /// Returns array of validation errors, respects stop_on_first_error flag
216    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
217        let mut all_errors = Vec::new();
218
219        // C1: Repetitive Sequence Count
220        if let Some(error) = self.validate_c1_repetitive_sequence_count() {
221            all_errors.push(error);
222            if stop_on_first_error {
223                return all_errors;
224            }
225        }
226
227        // C2: Mutual Exclusivity of 50a and 52a
228        let c2_errors = self.validate_c2_mutual_exclusivity();
229        all_errors.extend(c2_errors);
230        if stop_on_first_error && !all_errors.is_empty() {
231            return all_errors;
232        }
233
234        // C3: Currency Consistency
235        if let Some(error) = self.validate_c3_currency_consistency() {
236            all_errors.push(error);
237        }
238
239        all_errors
240    }
241}
242
243impl crate::traits::SwiftMessageBody for MT210 {
244    fn message_type() -> &'static str {
245        "210"
246    }
247
248    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
249        Self::parse_from_block4(block4)
250    }
251
252    fn to_mt_string(&self) -> String {
253        let mut result = String::new();
254
255        append_field(&mut result, &self.transaction_reference);
256        append_optional_field(&mut result, &self.account_identification);
257        append_field(&mut result, &self.value_date);
258
259        // Transactions
260        for txn in &self.transactions {
261            append_optional_field(&mut result, &txn.related_reference);
262            append_field(&mut result, &txn.currency_amount);
263            append_optional_field(&mut result, &txn.ordering_customer);
264            append_optional_field(&mut result, &txn.ordering_institution);
265            append_optional_field(&mut result, &txn.intermediary);
266        }
267
268        finalize_mt_string(result, false)
269    }
270
271    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
272        // Call the existing public method implementation
273        MT210::validate_network_rules(self, stop_on_first_error)
274    }
275}