swift_mt_message/messages/
mt110.rs

1use crate::errors::SwiftValidationError;
2use crate::fields::Field52DrawerBank;
3use crate::fields::*;
4use crate::parser::utils::*;
5use serde::{Deserialize, Serialize};
6
7/// Cheque details (repeating sequence)
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct MT110Cheque {
10    /// Cheque number (Field 21)
11    #[serde(rename = "21")]
12    pub field_21: Field21NoOption,
13
14    /// Date of issue (Field 30)
15    #[serde(rename = "30")]
16    pub field_30: Field30,
17
18    /// Amount (Field 32)
19    #[serde(flatten)]
20    pub field_32: Field32AB,
21
22    /// Payer (Field 50)
23    #[serde(flatten, skip_serializing_if = "Option::is_none")]
24    pub field_50: Option<Field50OrderingCustomerAFK>,
25
26    /// Drawer bank (Field 52)
27    #[serde(flatten, skip_serializing_if = "Option::is_none")]
28    pub field_52: Option<Field52DrawerBank>,
29
30    /// Payee (Field 59)
31    #[serde(flatten)]
32    pub field_59: Field59,
33}
34
35/// **MT110: Advice of Cheque(s)**
36///
37/// Advice from drawer bank to drawee bank confirming issuance of one or more cheques.
38/// Supports multiple cheque details in a single message.
39///
40/// **Usage:** Cheque issuance advice, payment notifications
41/// **Category:** Category 1 (Customer Payments)
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct MT110 {
44    /// Sender's reference (Field 20)
45    #[serde(rename = "20")]
46    pub field_20: Field20,
47
48    /// Sender's correspondent (Field 53)
49    #[serde(flatten, skip_serializing_if = "Option::is_none")]
50    pub field_53a: Option<Field53SenderCorrespondent>,
51
52    /// Receiver's correspondent (Field 54)
53    #[serde(flatten, skip_serializing_if = "Option::is_none")]
54    pub field_54a: Option<Field54ReceiverCorrespondent>,
55
56    /// Sender to receiver information (Field 72)
57    #[serde(rename = "72", skip_serializing_if = "Option::is_none")]
58    pub field_72: Option<Field72>,
59
60    /// Cheque details (max 10)
61    #[serde(rename = "#", default)]
62    pub cheques: Vec<MT110Cheque>,
63}
64
65impl MT110 {
66    /// Parse message from Block 4 content
67    pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
68        let mut parser = crate::parser::MessageParser::new(block4, "110");
69
70        // Parse mandatory fields
71        let field_20 = parser.parse_field::<Field20>("20")?;
72
73        // Parse optional fields before cheque details
74        let field_53a = parser.parse_optional_variant_field::<Field53SenderCorrespondent>("53")?;
75        let field_54a =
76            parser.parse_optional_variant_field::<Field54ReceiverCorrespondent>("54")?;
77        let field_72 = parser.parse_optional_field::<Field72>("72")?;
78
79        // Parse cheque details - enable duplicates for repeating fields
80        let mut cheques = Vec::new();
81        parser = parser.with_duplicates(true);
82
83        // Parse each cheque detail - they start with field 21
84        while parser.detect_field("21") {
85            let field_21 = parser.parse_field::<Field21NoOption>("21")?;
86            let field_30 = parser.parse_field::<Field30>("30")?;
87
88            // Parse field 32 (amount) - only A or B per spec
89            let field_32 = parser.parse_variant_field::<Field32AB>("32")?;
90
91            // Parse optional fields
92            let field_50 =
93                parser.parse_optional_variant_field::<Field50OrderingCustomerAFK>("50")?;
94            let field_52 = parser.parse_optional_variant_field::<Field52DrawerBank>("52")?;
95
96            // Parse field 59 (payee)
97            let field_59 = parser.parse_variant_field::<Field59>("59")?;
98
99            cheques.push(MT110Cheque {
100                field_21,
101                field_30,
102                field_32,
103                field_50,
104                field_52,
105                field_59,
106            });
107        }
108
109        // Validate we have at least one cheque detail
110        if cheques.is_empty() {
111            return Err(crate::errors::ParseError::InvalidFormat {
112                message: "MT110: At least one cheque detail is required".to_string(),
113            });
114        }
115
116        // Note: Max 10 repetitions (NVR C1) and currency consistency (NVR C2)
117        // are validated in validate_network_rules(), not during parsing
118
119        Ok(MT110 {
120            field_20,
121            field_53a,
122            field_54a,
123            field_72,
124            cheques,
125        })
126    }
127
128    /// Parse from generic SWIFT input (tries to detect blocks)
129    pub fn parse(input: &str) -> Result<Self, crate::errors::ParseError> {
130        let block4 = extract_block4(input)?;
131        Self::parse_from_block4(&block4)
132    }
133
134    // ========================================================================
135    // NETWORK VALIDATION RULES (SR 2025 MT110)
136    // ========================================================================
137
138    /// C1: Maximum 10 repetitive sequences (Error code: T10)
139    /// The repetitive sequence must not be present more than ten times
140    fn validate_c1_max_repetitions(&self) -> Option<SwiftValidationError> {
141        if self.cheques.len() > 10 {
142            return Some(SwiftValidationError::content_error(
143                "T10",
144                "21-59a",
145                "",
146                &format!(
147                    "The repetitive sequence (cheque details) appears {} times, but maximum 10 occurrences are allowed",
148                    self.cheques.len()
149                ),
150                "The repetitive sequence containing fields 21, 30, 32a, 50a, 52a, and 59a must not be present more than ten times in the message",
151            ));
152        }
153
154        None
155    }
156
157    /// C2: Currency Code Consistency (Error code: C02)
158    /// The currency code in field 32a must be the same for all occurrences
159    fn validate_c2_currency_consistency(&self) -> Option<SwiftValidationError> {
160        if self.cheques.is_empty() {
161            return None;
162        }
163
164        // Get currency from first cheque
165        let first_currency = match &self.cheques[0].field_32 {
166            Field32AB::A(amt) => &amt.currency,
167            Field32AB::B(amt) => &amt.currency,
168        };
169
170        // Check all subsequent cheques have the same currency
171        for (idx, cheque) in self.cheques.iter().enumerate().skip(1) {
172            let cheque_currency = match &cheque.field_32 {
173                Field32AB::A(amt) => &amt.currency,
174                Field32AB::B(amt) => &amt.currency,
175            };
176
177            if cheque_currency != first_currency {
178                return Some(SwiftValidationError::content_error(
179                    "C02",
180                    "32a",
181                    cheque_currency,
182                    &format!(
183                        "Cheque {}: Currency code in field 32a ({}) must be the same as in other occurrences ({})",
184                        idx + 1,
185                        cheque_currency,
186                        first_currency
187                    ),
188                    "The currency code in the amount field 32a must be the same for all occurrences of this field in the message",
189                ));
190            }
191        }
192
193        None
194    }
195
196    /// Main validation method - validates all network rules
197    /// Returns array of validation errors, respects stop_on_first_error flag
198    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
199        let mut all_errors = Vec::new();
200
201        // C1: Maximum 10 repetitions
202        if let Some(error) = self.validate_c1_max_repetitions() {
203            all_errors.push(error);
204            if stop_on_first_error {
205                return all_errors;
206            }
207        }
208
209        // C2: Currency Code Consistency
210        if let Some(error) = self.validate_c2_currency_consistency() {
211            all_errors.push(error);
212        }
213
214        all_errors
215    }
216}
217
218impl crate::traits::SwiftMessageBody for MT110 {
219    fn message_type() -> &'static str {
220        "110"
221    }
222
223    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
224        // Call the existing public method implementation
225        MT110::parse_from_block4(block4)
226    }
227
228    fn to_mt_string(&self) -> String {
229        let mut result = String::new();
230
231        // Add header fields
232        append_field(&mut result, &self.field_20);
233        append_optional_field(&mut result, &self.field_53a);
234        append_optional_field(&mut result, &self.field_54a);
235        append_optional_field(&mut result, &self.field_72);
236
237        // Add cheque details in sequence
238        for cheque in &self.cheques {
239            append_field(&mut result, &cheque.field_21);
240            append_field(&mut result, &cheque.field_30);
241            append_field(&mut result, &cheque.field_32);
242            append_optional_field(&mut result, &cheque.field_50);
243            append_optional_field(&mut result, &cheque.field_52);
244            append_field(&mut result, &cheque.field_59);
245        }
246
247        finalize_mt_string(result, false)
248    }
249
250    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
251        // Call the existing public method implementation
252        MT110::validate_network_rules(self, stop_on_first_error)
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_validate_c2_currency_consistency_pass() {
262        // Valid message with consistent currency
263        let input = r#":20:TESTREF123
264:21:CHQ001
265:30:250101
266:32B:USD1234,56
267:59:JOHN DOE
268123 MAIN ST
269NEW YORK
270:21:CHQ002
271:30:250102
272:32B:USD2345,67
273:59:JANE SMITH
274456 ELM ST
275BOSTON
276-"#;
277
278        let msg = MT110::parse_from_block4(input).expect("Should parse successfully");
279        let errors = msg.validate_network_rules(false);
280        assert!(
281            errors.is_empty(),
282            "Should have no validation errors for consistent currencies"
283        );
284    }
285
286    #[test]
287    fn test_validate_c2_currency_consistency_fail() {
288        // Invalid message with inconsistent currency
289        let input = r#":20:TESTREF123
290:21:CHQ001
291:30:250101
292:32B:USD1234,56
293:59:JOHN DOE
294123 MAIN ST
295NEW YORK
296:21:CHQ002
297:30:250102
298:32B:EUR2345,67
299:59:JANE SMITH
300456 ELM ST
301BOSTON
302-"#;
303
304        let msg = MT110::parse_from_block4(input).expect("Should parse successfully");
305        let errors = msg.validate_network_rules(false);
306        assert_eq!(errors.len(), 1, "Should have exactly one validation error");
307        assert_eq!(errors[0].code(), "C02");
308        assert!(errors[0].message().contains("Currency code in field 32a"));
309    }
310
311    #[test]
312    fn test_validate_c1_max_repetitions() {
313        // Create a message with 11 cheques (exceeds limit)
314        let mut cheque_details = String::new();
315        for i in 1..=11 {
316            cheque_details.push_str(&format!(
317                r#":21:CHQ{:03}
318:30:250101
319:32B:USD100,00
320:59:PAYEE {}
321ADDRESS LINE
322CITY
323"#,
324                i, i
325            ));
326        }
327
328        let input = format!(":20:TESTREF123\n{}-", cheque_details);
329
330        let msg = MT110::parse_from_block4(&input).expect("Should parse successfully");
331        let errors = msg.validate_network_rules(false);
332        assert_eq!(errors.len(), 1, "Should have exactly one validation error");
333        assert_eq!(errors[0].code(), "T10");
334        assert!(errors[0].message().contains("maximum 10 occurrences"));
335    }
336
337    #[test]
338    fn test_validate_stop_on_first_error() {
339        // Create a message with both errors: too many cheques AND inconsistent currency
340        let mut cheque_details = String::new();
341        for i in 1..=11 {
342            let currency = if i % 2 == 0 { "EUR" } else { "USD" };
343            cheque_details.push_str(&format!(
344                r#":21:CHQ{:03}
345:30:250101
346:32B:{}100,00
347:59:PAYEE {}
348ADDRESS LINE
349CITY
350"#,
351                i, currency, i
352            ));
353        }
354
355        let input = format!(":20:TESTREF123\n{}-", cheque_details);
356
357        let msg = MT110::parse_from_block4(&input).expect("Should parse successfully");
358
359        // With stop_on_first_error = true, should only get first error
360        let errors_stop = msg.validate_network_rules(true);
361        assert_eq!(errors_stop.len(), 1, "Should stop after first error");
362
363        // With stop_on_first_error = false, should get all errors
364        let errors_all = msg.validate_network_rules(false);
365        assert_eq!(errors_all.len(), 2, "Should collect all errors");
366        assert!(errors_all.iter().any(|e| e.code() == "T10"));
367        assert!(errors_all.iter().any(|e| e.code() == "C02"));
368    }
369}