swift_mt_message/messages/
mt204.rs

1use crate::errors::ParseError;
2use crate::errors::SwiftValidationError;
3use crate::fields::*;
4use crate::parser::MessageParser;
5use crate::parser::utils::*;
6use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8
9/// **MT204: Financial Markets Direct Debit Message**
10///
11/// Direct debit transactions in financial markets for clearing and settlement.
12///
13/// **Usage:** Multiple transaction clearing, settlement batches
14/// **Category:** Category 2 (Financial Institution Transfers)
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct MT204 {
17    /// Transaction Reference Number (Field 20)
18    #[serde(rename = "20")]
19    pub transaction_reference: Field20,
20
21    /// Sum of Amounts (Field 19)
22    #[serde(rename = "19")]
23    pub sum_of_amounts: Field19,
24
25    /// Execution Date (Field 30)
26    #[serde(rename = "30")]
27    pub execution_date: Field30,
28
29    /// Account With Institution (Field 57)
30    #[serde(flatten, skip_serializing_if = "Option::is_none")]
31    pub account_with_institution: Option<Field57>,
32
33    /// Beneficiary Institution (Field 58)
34    #[serde(flatten, skip_serializing_if = "Option::is_none")]
35    pub beneficiary_institution: Option<Field58>,
36
37    /// Sender to Receiver Information (Field 72)
38    #[serde(rename = "72", skip_serializing_if = "Option::is_none")]
39    pub sender_to_receiver: Option<Field72>,
40
41    /// Transactions (repeatable)
42    #[serde(rename = "#", default)]
43    pub transactions: Vec<MT204Transaction>,
44}
45
46/// Individual transaction within MT204
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct MT204Transaction {
49    /// Transaction Reference Number (Field 20)
50    #[serde(rename = "20")]
51    pub transaction_reference: Field20,
52
53    /// Related Reference (Field 21)
54    #[serde(rename = "21", skip_serializing_if = "Option::is_none")]
55    pub related_reference: Option<Field21NoOption>,
56
57    /// Currency Code, Amount (Field 32B)
58    #[serde(rename = "32B")]
59    pub currency_amount: Field32B,
60
61    /// Sender's Correspondent (Field 53)
62    #[serde(flatten, skip_serializing_if = "Option::is_none")]
63    pub senders_correspondent: Option<Field53>,
64
65    /// Sender to Receiver Information (Field 72)
66    #[serde(rename = "72", skip_serializing_if = "Option::is_none")]
67    pub sender_to_receiver: Option<Field72>,
68}
69
70impl MT204 {
71    /// Parse MT204 from a raw SWIFT message string
72    pub fn parse_from_block4(block4: &str) -> Result<Self, ParseError> {
73        let mut parser = MessageParser::new(block4, "204");
74
75        // Parse header fields in the correct order (matching to_mt_string)
76        let sum_of_amounts = parser.parse_field::<Field19>("19")?;
77        let transaction_reference = parser.parse_field::<Field20>("20")?;
78        let execution_date = parser.parse_field::<Field30>("30")?;
79
80        // Parse optional Field 57 - Account With Institution
81        let account_with_institution = parser.parse_optional_variant_field::<Field57>("57")?;
82
83        // Parse optional Field 58 - Beneficiary Institution
84        let beneficiary_institution = parser.parse_optional_variant_field::<Field58>("58")?;
85
86        // Parse optional Field 72 at message level
87        let sender_to_receiver = parser.parse_optional_field::<Field72>("72")?;
88
89        // Parse repeating transaction sequences - enable duplicates mode
90        parser = parser.with_duplicates(true);
91        let mut transactions = Vec::new();
92
93        while parser.detect_field("20") {
94            // Parse mandatory Field 20 - Transaction Reference Number
95            let transaction_reference = parser.parse_field::<Field20>("20")?;
96
97            // Parse optional Field 21 - Related Reference
98            let related_reference = parser.parse_optional_field::<Field21NoOption>("21")?;
99
100            // Parse mandatory Field 32B - Currency Code, Amount
101            let currency_amount = parser.parse_field::<Field32B>("32B")?;
102
103            // Parse optional Field 53 - Sender's Correspondent
104            let senders_correspondent = parser.parse_optional_variant_field::<Field53>("53")?;
105
106            // Parse optional Field 72 - Sender to Receiver Information (transaction level)
107            let sender_to_receiver = parser.parse_optional_field::<Field72>("72")?;
108
109            transactions.push(MT204Transaction {
110                transaction_reference,
111                related_reference,
112                currency_amount,
113                senders_correspondent,
114                sender_to_receiver,
115            });
116
117            // Limit to 10 sequences per validation rules
118            if transactions.len() >= 10 {
119                break;
120            }
121        }
122
123        Ok(MT204 {
124            transaction_reference,
125            sum_of_amounts,
126            execution_date,
127            account_with_institution,
128            beneficiary_institution,
129            sender_to_receiver,
130            transactions,
131        })
132    }
133
134    // ========================================================================
135    // NETWORK VALIDATION RULES (SR 2025 MT204)
136    // ========================================================================
137
138    /// Maximum number of repetitive sequences allowed
139    const MAX_SEQUENCE_B_OCCURRENCES: usize = 10;
140
141    // ========================================================================
142    // HELPER METHODS
143    // ========================================================================
144
145    /// Get the sum of all transaction amounts in Sequence B
146    fn calculate_sum_of_transactions(&self) -> f64 {
147        self.transactions
148            .iter()
149            .map(|tx| tx.currency_amount.amount)
150            .sum()
151    }
152
153    /// Get all unique currency codes from Sequence B transactions
154    fn get_transaction_currencies(&self) -> HashSet<String> {
155        self.transactions
156            .iter()
157            .map(|tx| tx.currency_amount.currency.clone())
158            .collect()
159    }
160
161    // ========================================================================
162    // VALIDATION RULES (C1-C3, T10)
163    // ========================================================================
164
165    /// C1: Sum of Amounts Must Equal Total of Transaction Amounts (Error code: C01)
166    /// The amount in field 19 must equal the sum of amounts in all occurrences of field 32B
167    fn validate_c1_sum_of_amounts(&self) -> Option<SwiftValidationError> {
168        if self.transactions.is_empty() {
169            return None; // No transactions to validate
170        }
171
172        let sum_of_transactions = self.calculate_sum_of_transactions();
173        let field_19_amount = self.sum_of_amounts.amount;
174
175        // Use a small epsilon for floating-point comparison (0.01 = 1 cent)
176        let difference = (field_19_amount - sum_of_transactions).abs();
177
178        if difference > 0.01 {
179            return Some(SwiftValidationError::content_error(
180                "C01",
181                "19",
182                &field_19_amount.to_string(),
183                &format!(
184                    "Sum of amounts in field 19 ({:.2}) must equal the sum of all field 32B amounts ({:.2}). Difference: {:.2}",
185                    field_19_amount, sum_of_transactions, difference
186                ),
187                "The amount in field 19 must equal the sum of the amounts in all occurrences of field 32B",
188            ));
189        }
190
191        None
192    }
193
194    /// C2: Currency Code Consistency Across All Transactions (Error code: C02)
195    /// The currency code in field 32B must be the same for all occurrences
196    fn validate_c2_currency_consistency(&self) -> Option<SwiftValidationError> {
197        if self.transactions.is_empty() {
198            return None;
199        }
200
201        let currencies = self.get_transaction_currencies();
202
203        if currencies.len() > 1 {
204            let currency_list: Vec<String> = currencies.into_iter().collect();
205            return Some(SwiftValidationError::content_error(
206                "C02",
207                "32B",
208                &currency_list.join(", "),
209                &format!(
210                    "All occurrences of field 32B must have the same currency code. Found currencies: {}",
211                    currency_list.join(", ")
212                ),
213                "The currency code in the amount field 32B must be the same for all occurrences of this field in the message",
214            ));
215        }
216
217        None
218    }
219
220    /// C3/T10: Maximum Number of Repetitive Sequences (Error code: T10)
221    /// Sequence B must not appear more than ten times
222    fn validate_c3_max_sequences(&self) -> Option<SwiftValidationError> {
223        let count = self.transactions.len();
224
225        if count > Self::MAX_SEQUENCE_B_OCCURRENCES {
226            return Some(SwiftValidationError::content_error(
227                "T10",
228                "Sequence B",
229                &count.to_string(),
230                &format!(
231                    "The repetitive sequence B appears {} times, which exceeds the maximum of {} occurrences",
232                    count,
233                    Self::MAX_SEQUENCE_B_OCCURRENCES
234                ),
235                "The repetitive sequence must not appear more than ten times",
236            ));
237        }
238
239        None
240    }
241
242    /// Main validation method - validates all network rules
243    /// Returns array of validation errors, respects stop_on_first_error flag
244    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
245        let mut all_errors = Vec::new();
246
247        // C1: Sum of Amounts
248        if let Some(error) = self.validate_c1_sum_of_amounts() {
249            all_errors.push(error);
250            if stop_on_first_error {
251                return all_errors;
252            }
253        }
254
255        // C2: Currency Consistency
256        if let Some(error) = self.validate_c2_currency_consistency() {
257            all_errors.push(error);
258            if stop_on_first_error {
259                return all_errors;
260            }
261        }
262
263        // C3/T10: Maximum Sequences
264        if let Some(error) = self.validate_c3_max_sequences() {
265            all_errors.push(error);
266            if stop_on_first_error {
267                return all_errors;
268            }
269        }
270
271        all_errors
272    }
273}
274
275impl crate::traits::SwiftMessageBody for MT204 {
276    fn message_type() -> &'static str {
277        "204"
278    }
279
280    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
281        // Call the existing public method implementation
282        MT204::parse_from_block4(block4)
283    }
284
285    fn to_mt_string(&self) -> String {
286        // Call the existing public method implementation
287        let mut result = String::new();
288
289        append_field(&mut result, &self.sum_of_amounts);
290        append_field(&mut result, &self.transaction_reference);
291        append_field(&mut result, &self.execution_date);
292        append_optional_field(&mut result, &self.account_with_institution);
293        append_optional_field(&mut result, &self.beneficiary_institution);
294        append_optional_field(&mut result, &self.sender_to_receiver);
295
296        // Transactions
297        for txn in &self.transactions {
298            append_field(&mut result, &txn.transaction_reference);
299            append_optional_field(&mut result, &txn.related_reference);
300            append_field(&mut result, &txn.currency_amount);
301            append_optional_field(&mut result, &txn.senders_correspondent);
302            append_optional_field(&mut result, &txn.sender_to_receiver);
303        }
304
305        finalize_mt_string(result, false)
306    }
307
308    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
309        // Call the existing public method implementation
310        MT204::validate_network_rules(self, stop_on_first_error)
311    }
312}