swift_mt_message/messages/
mt942.rs

1use crate::errors::SwiftValidationError;
2use crate::fields::*;
3use crate::parser::utils::*;
4use serde::{Deserialize, Serialize};
5
6/// **MT942: Interim Transaction Report**
7///
8/// Intraday statement with current balance and transaction details.
9///
10/// **Usage:** Intraday reporting, real-time cash positioning
11/// **Category:** Category 9 (Cash Management & Customer Status)
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct MT942 {
14    /// Transaction Reference Number (Field 20)
15    #[serde(rename = "20")]
16    pub field_20: Field20,
17
18    /// Related Reference (Field 21)
19    #[serde(rename = "21", skip_serializing_if = "Option::is_none")]
20    pub field_21: Option<Field21NoOption>,
21
22    /// Account Identification (Field 25)
23    #[serde(rename = "25")]
24    pub field_25: Field25AccountIdentification,
25
26    /// Statement Number/Sequence Number (Field 28C)
27    #[serde(rename = "28C")]
28    pub field_28c: Field28C,
29
30    /// Debit Floor Limit Indicator (Field 34F)
31    #[serde(rename = "34F_debit")]
32    pub floor_limit_debit: Field34F,
33
34    /// Credit Floor Limit Indicator (Field 34F)
35    #[serde(rename = "34F_credit", skip_serializing_if = "Option::is_none")]
36    pub floor_limit_credit: Option<Field34F>,
37
38    /// Date/Time Indication (Field 13D)
39    #[serde(rename = "13D")]
40    pub field_13d: Field13D,
41
42    /// Statement lines
43    #[serde(rename = "statement_lines")]
44    pub statement_lines: Vec<MT942StatementLine>,
45
46    /// Number and Sum of Debits (Field 90D)
47    #[serde(rename = "90D", skip_serializing_if = "Option::is_none")]
48    pub field_90d: Option<Field90D>,
49
50    /// Number and Sum of Credits (Field 90C)
51    #[serde(rename = "90C", skip_serializing_if = "Option::is_none")]
52    pub field_90c: Option<Field90C>,
53
54    /// Information to Account Owner (Field 86)
55    #[serde(rename = "86", skip_serializing_if = "Option::is_none")]
56    pub field_86: Option<Field86>,
57}
58
59/// Statement line for MT942
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct MT942StatementLine {
62    /// Statement Line (Field 61)
63    #[serde(rename = "61")]
64    pub field_61: Field61,
65
66    /// Information to Account Owner (Field 86)
67    #[serde(rename = "86", skip_serializing_if = "Option::is_none")]
68    pub field_86: Option<Field86>,
69}
70
71impl MT942 {
72    /// Parse message from Block 4 content
73    pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
74        let mut parser = crate::parser::MessageParser::new(block4, "942");
75
76        // Parse mandatory fields in flexible order
77        // Field 13D might appear first due to HashMap ordering issues
78
79        // Check if Field 13D appears early (out of standard order)
80        let field_13d_early = if parser.detect_field("13D") {
81            Some(parser.parse_field::<Field13D>("13D")?)
82        } else {
83            None
84        };
85
86        // Parse fields in standard order
87        let field_20 = parser.parse_field::<Field20>("20")?;
88        let field_21 = parser.parse_optional_field::<Field21NoOption>("21")?;
89        let field_25 = parser.parse_field::<Field25AccountIdentification>("25")?;
90        let field_28c = parser.parse_field::<Field28C>("28C")?;
91
92        // Parse floor limit indicators (Field 34F appears twice)
93        let floor_limit_debit = parser.parse_field::<Field34F>("34F")?;
94        let floor_limit_credit = parser.parse_optional_field::<Field34F>("34F")?;
95
96        // Parse Field 13D if not already parsed
97        let field_13d = if let Some(early_13d) = field_13d_early {
98            early_13d
99        } else {
100            parser.parse_field::<Field13D>("13D")?
101        };
102
103        // Enable duplicate field handling for statement lines
104        parser = parser.with_duplicates(true);
105
106        // Parse statement lines (optional, repetitive)
107        let mut statement_lines = Vec::new();
108
109        while parser.detect_field("61") {
110            let field_61 = parser.parse_field::<Field61>("61")?;
111            let field_86 = parser.parse_optional_field::<Field86>("86")?;
112
113            statement_lines.push(MT942StatementLine { field_61, field_86 });
114        }
115
116        // Parse optional summary fields
117        let field_90d = parser.parse_optional_field::<Field90D>("90D")?;
118        let field_90c = parser.parse_optional_field::<Field90C>("90C")?;
119
120        // Parse optional information to account owner
121        let field_86 = parser.parse_optional_field::<Field86>("86")?;
122
123        Ok(MT942 {
124            field_20,
125            field_21,
126            field_25,
127            field_28c,
128            floor_limit_debit,
129            floor_limit_credit,
130            field_13d,
131            statement_lines,
132            field_90d,
133            field_90c,
134            field_86,
135        })
136    }
137
138    // ========================================================================
139    // NETWORK VALIDATION RULES (SR 2025 MT942)
140    // ========================================================================
141
142    // ========================================================================
143    // HELPER METHODS
144    // ========================================================================
145
146    /// Get the base currency from the mandatory debit floor limit
147    fn get_base_currency(&self) -> &str {
148        &self.floor_limit_debit.currency[0..2]
149    }
150
151    // ========================================================================
152    // VALIDATION RULES (C1-C3)
153    // ========================================================================
154
155    /// C1: Currency Code Consistency (Error code: C27)
156    /// The first two characters of the three character currency code in fields 34F,
157    /// 90D, and 90C must be the same for all occurrences
158    fn validate_c1_currency_consistency(&self) -> Vec<SwiftValidationError> {
159        let mut errors = Vec::new();
160        let base_currency = self.get_base_currency();
161
162        // Check floor limit credit if present
163        if let Some(ref floor_limit_credit) = self.floor_limit_credit {
164            let credit_currency = &floor_limit_credit.currency[0..2];
165            if credit_currency != base_currency {
166                errors.push(SwiftValidationError::content_error(
167                    "C27",
168                    "34F",
169                    &floor_limit_credit.currency,
170                    &format!(
171                        "Currency code in second field 34F ({}) must match first field 34F ({}). First two characters must be the same for all currency fields",
172                        credit_currency, base_currency
173                    ),
174                    "The first two characters of the three character currency code in fields 34F, 90D, and 90C must be the same for all occurrences",
175                ));
176            }
177        }
178
179        // Check field 90D if present
180        if let Some(ref field_90d) = self.field_90d {
181            let field_90d_currency = &field_90d.currency[0..2];
182            if field_90d_currency != base_currency {
183                errors.push(SwiftValidationError::content_error(
184                    "C27",
185                    "90D",
186                    &field_90d.currency,
187                    &format!(
188                        "Currency code in field 90D ({}) must match field 34F ({}). First two characters must be the same for all currency fields",
189                        field_90d_currency, base_currency
190                    ),
191                    "The first two characters of the three character currency code in fields 34F, 90D, and 90C must be the same for all occurrences",
192                ));
193            }
194        }
195
196        // Check field 90C if present
197        if let Some(ref field_90c) = self.field_90c {
198            let field_90c_currency = &field_90c.currency[0..2];
199            if field_90c_currency != base_currency {
200                errors.push(SwiftValidationError::content_error(
201                    "C27",
202                    "90C",
203                    &field_90c.currency,
204                    &format!(
205                        "Currency code in field 90C ({}) must match field 34F ({}). First two characters must be the same for all currency fields",
206                        field_90c_currency, base_currency
207                    ),
208                    "The first two characters of the three character currency code in fields 34F, 90D, and 90C must be the same for all occurrences",
209                ));
210            }
211        }
212
213        errors
214    }
215
216    /// C2: Floor Limit Indicator D/C Mark (Error code: C23)
217    /// When only one field 34F is present, the second subfield (D/C Mark) must not be used.
218    /// When both fields 34F are present, subfield 2 of the first 34F must contain 'D',
219    /// and subfield 2 of the second 34F must contain 'C'
220    fn validate_c2_floor_limit_dc_mark(&self) -> Option<SwiftValidationError> {
221        if let Some(ref floor_limit_credit) = self.floor_limit_credit {
222            // Two occurrences - first must have 'D', second must have 'C'
223
224            // Check first occurrence (debit) has 'D'
225            if self.floor_limit_debit.indicator != Some('D') {
226                return Some(SwiftValidationError::content_error(
227                    "C23",
228                    "34F",
229                    &format!("{:?}", self.floor_limit_debit.indicator),
230                    &format!(
231                        "When two field 34F are present, first occurrence must have D/C mark 'D', found '{:?}'",
232                        self.floor_limit_debit.indicator
233                    ),
234                    "When both fields 34F are present, subfield 2 of the first 34F must contain the value 'D', and subfield 2 of the second 34F must contain the value 'C'",
235                ));
236            }
237
238            // Check second occurrence (credit) has 'C'
239            if floor_limit_credit.indicator != Some('C') {
240                return Some(SwiftValidationError::content_error(
241                    "C23",
242                    "34F",
243                    &format!("{:?}", floor_limit_credit.indicator),
244                    &format!(
245                        "When two field 34F are present, second occurrence must have D/C mark 'C', found '{:?}'",
246                        floor_limit_credit.indicator
247                    ),
248                    "When both fields 34F are present, subfield 2 of the first 34F must contain the value 'D', and subfield 2 of the second 34F must contain the value 'C'",
249                ));
250            }
251        } else {
252            // Single occurrence - D/C mark must not be used
253            if self.floor_limit_debit.indicator.is_some() {
254                return Some(SwiftValidationError::content_error(
255                    "C23",
256                    "34F",
257                    &format!("{:?}", self.floor_limit_debit.indicator),
258                    &format!(
259                        "When only one field 34F is present, D/C mark must not be used, found '{:?}'",
260                        self.floor_limit_debit.indicator
261                    ),
262                    "When only one field 34F is present, the second subfield (D/C Mark) must not be used",
263                ));
264            }
265        }
266
267        None
268    }
269
270    /// C3: Field 86 Positioning and Relationship to Field 61 (Error code: C24)
271    /// If field 86 is present in any occurrence of the repetitive sequence, it must be
272    /// preceded by a field 61 except if that field 86 is the last field in the message,
273    /// then field 61 is optional
274    fn validate_c3_field_86_positioning(&self) -> Vec<SwiftValidationError> {
275        let errors = Vec::new();
276
277        // Check each statement line
278        for statement_line in self.statement_lines.iter() {
279            if statement_line.field_86.is_some() {
280                // Within the repetitive sequence, field 86 must be preceded by field 61
281                // This is structurally enforced by our data model (field_86 is part of MT942StatementLine)
282                // So this check is always satisfied for statement_lines
283
284                // The rule is primarily about ensuring field 86 within statement lines
285                // is properly associated with a field 61, which our structure guarantees
286            }
287        }
288
289        // If there's a message-level field 86 (self.field_86), it's the last field
290        // and doesn't need to be preceded by field 61, so it's valid
291
292        // The structural validation is implicitly handled by the parsing logic
293        // No explicit validation error needed here as the structure enforces the rule
294
295        errors
296    }
297
298    /// Main validation method - validates all network rules
299    /// Returns array of validation errors, respects stop_on_first_error flag
300    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
301        let mut all_errors = Vec::new();
302
303        // C1: Currency Code Consistency
304        let c1_errors = self.validate_c1_currency_consistency();
305        all_errors.extend(c1_errors);
306        if stop_on_first_error && !all_errors.is_empty() {
307            return all_errors;
308        }
309
310        // C2: Floor Limit Indicator D/C Mark
311        if let Some(error) = self.validate_c2_floor_limit_dc_mark() {
312            all_errors.push(error);
313            if stop_on_first_error {
314                return all_errors;
315            }
316        }
317
318        // C3: Field 86 Positioning
319        let c3_errors = self.validate_c3_field_86_positioning();
320        all_errors.extend(c3_errors);
321
322        all_errors
323    }
324}
325
326// Implement the SwiftMessageBody trait for MT942
327impl crate::traits::SwiftMessageBody for MT942 {
328    fn message_type() -> &'static str {
329        "942"
330    }
331
332    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
333        Self::parse_from_block4(block4)
334    }
335
336    fn to_mt_string(&self) -> String {
337        let mut result = String::new();
338
339        append_field(&mut result, &self.field_20);
340        append_optional_field(&mut result, &self.field_21);
341        append_field(&mut result, &self.field_25);
342        append_field(&mut result, &self.field_28c);
343        append_field(&mut result, &self.floor_limit_debit);
344        append_optional_field(&mut result, &self.floor_limit_credit);
345        append_field(&mut result, &self.field_13d);
346
347        // Statement lines
348        for statement_line in &self.statement_lines {
349            append_field(&mut result, &statement_line.field_61);
350            append_optional_field(&mut result, &statement_line.field_86);
351        }
352
353        append_optional_field(&mut result, &self.field_90d);
354        append_optional_field(&mut result, &self.field_90c);
355        append_optional_field(&mut result, &self.field_86);
356
357        finalize_mt_string(result, false)
358    }
359
360    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
361        // Call the existing public method implementation
362        MT942::validate_network_rules(self, stop_on_first_error)
363    }
364}