swift_mt_message/parser/
sequence_parser.rs

1//! Generic sequence parser for SWIFT MT messages with multiple sequences
2//!
3//! Many SWIFT MT messages have multiple sequences:
4//! - MT101: Sequence A (General Info), Sequence B (Transactions)
5//! - MT104: Sequence A (General Info), Sequence B (Transactions), Sequence C (Settlement)
6//! - MT107: Similar structure with multiple sequences
7//!
8//! This module provides generic parsing capabilities for such messages.
9
10use crate::errors::Result;
11use std::collections::HashMap;
12
13/// Type alias for field storage to reduce complexity
14pub type FieldMap = HashMap<String, Vec<(String, usize)>>;
15
16/// Configuration for sequence parsing
17#[derive(Debug, Clone)]
18pub struct SequenceConfig {
19    /// Field that marks the start of sequence B (usually "21")
20    pub sequence_b_marker: String,
21    /// Fields that belong exclusively to sequence C (if any)
22    pub sequence_c_fields: Vec<String>,
23    /// Whether sequence C exists for this message type
24    pub has_sequence_c: bool,
25}
26
27impl Default for SequenceConfig {
28    fn default() -> Self {
29        Self {
30            sequence_b_marker: "21".to_string(),
31            sequence_c_fields: vec![],
32            has_sequence_c: false,
33        }
34    }
35}
36
37/// Parsed sequences from a SWIFT message
38#[derive(Debug)]
39pub struct ParsedSequences {
40    /// Sequence A fields (general information)
41    pub sequence_a: FieldMap,
42    /// Sequence B fields (repetitive items like transactions)
43    pub sequence_b: FieldMap,
44    /// Sequence C fields (optional settlement/summary information)
45    pub sequence_c: FieldMap,
46}
47
48/// Split fields into sequences based on configuration
49pub fn split_into_sequences(fields: &FieldMap, config: &SequenceConfig) -> Result<ParsedSequences> {
50    let mut seq_a = HashMap::new();
51    let mut seq_b = HashMap::new();
52    let mut seq_c = HashMap::new();
53
54    // Get all fields sorted by position
55    let mut all_fields: Vec<(&str, &(String, usize))> = Vec::new();
56    for (tag, values) in fields {
57        for value in values {
58            all_fields.push((tag.as_str(), value));
59        }
60    }
61    all_fields.sort_by_key(|(_, (_, pos))| *pos);
62
63    // Find sequence boundaries
64    let mut first_b_marker_pos = None;
65    let mut _last_b_marker_pos = None;
66
67    // Special handling for MT935 which uses "23" or "25" as sequence markers
68    let secondary_marker = if config.sequence_b_marker == "23" {
69        Some("25")
70    } else {
71        None
72    };
73
74    // Fields that belong to sequence A even if they appear after sequence B
75    let sequence_a_fields = ["72", "77E", "79"];
76
77    for (tag, (_, pos)) in &all_fields {
78        if is_sequence_b_marker(tag, &config.sequence_b_marker)
79            || (secondary_marker.is_some() && *tag == secondary_marker.unwrap())
80        {
81            if first_b_marker_pos.is_none() {
82                first_b_marker_pos = Some(*pos);
83            }
84            _last_b_marker_pos = Some(*pos);
85        }
86    }
87
88    // Simpler approach: find all sequence B boundaries
89    // Sequence B starts at first field 21 and includes all fields until sequence C
90    let sequence_b_start_idx = all_fields.iter().position(|(tag, _)| {
91        is_sequence_b_marker(tag, &config.sequence_b_marker)
92            || (secondary_marker.is_some() && *tag == secondary_marker.unwrap())
93    });
94
95    // Find where sequence C would start (if it exists)
96    // This is tricky: sequence C fields appear after ALL transactions
97    // We need to find the last occurrence of transaction-ending fields
98    let mut sequence_c_start_idx: Option<usize> = None;
99
100    if config.has_sequence_c && sequence_b_start_idx.is_some() {
101        // Special handling for MT940/MT942 where sequence B contains statement lines
102        // and sequence C contains closing balance and summary fields
103        if config.sequence_b_marker == "61" {
104            // For MT940/MT942, look for the first occurrence of a sequence C field
105            // that is NOT field 86 (since 86 can appear in both sequences)
106            let seq_c_markers = config
107                .sequence_c_fields
108                .iter()
109                .filter(|f| *f != "86")
110                .collect::<Vec<_>>();
111
112            if let Some(seq_b_start) = sequence_b_start_idx {
113                for (i, (tag, _)) in all_fields.iter().enumerate().skip(seq_b_start) {
114                    let base_tag = tag.trim_end_matches(char::is_alphabetic);
115                    if seq_c_markers.iter().any(|marker| base_tag == *marker) {
116                        sequence_c_start_idx = Some(i);
117                        break;
118                    }
119                }
120            }
121        } else {
122            // Look for sequence C fields that appear after transaction patterns
123            // Transaction patterns typically end with fields like 59, 70, 71A
124            let transaction_end_fields = ["59", "70", "71A", "77B", "36"];
125
126            // Find the last occurrence of any transaction-ending field
127            let mut last_trans_end_idx: Option<usize> = None;
128            for (i, (tag, _)) in all_fields.iter().enumerate() {
129                let base_tag = tag.trim_end_matches(char::is_alphabetic);
130                if transaction_end_fields.contains(&base_tag) {
131                    last_trans_end_idx = Some(i);
132                }
133            }
134
135            // Look for sequence C fields after the last transaction end
136            if let Some(last_end) = last_trans_end_idx {
137                for (i, (tag, _)) in all_fields.iter().enumerate().skip(last_end + 1) {
138                    if config.sequence_c_fields.contains(&tag.to_string()) {
139                        sequence_c_start_idx = Some(i);
140                        break;
141                    }
142                }
143            } else {
144                // If no transaction-ending fields found, look for sequence C fields
145                // after the sequence B start
146                if let Some(seq_b_start) = sequence_b_start_idx {
147                    for (i, (tag, _)) in all_fields.iter().enumerate().skip(seq_b_start) {
148                        if config.sequence_c_fields.contains(&tag.to_string()) {
149                            sequence_c_start_idx = Some(i);
150                            break;
151                        }
152                    }
153                }
154            }
155        }
156    }
157
158    // Distribute fields to sequences based on boundaries
159    for (i, (tag, (value, pos))) in all_fields.iter().enumerate() {
160        // Check if this field should always be in sequence A
161        if sequence_a_fields.contains(tag) {
162            seq_a
163                .entry(tag.to_string())
164                .or_insert_with(Vec::new)
165                .push((value.clone(), *pos));
166            continue;
167        }
168
169        if let Some(seq_b_start) = sequence_b_start_idx {
170            if i < seq_b_start {
171                // Before sequence B = Sequence A
172                seq_a
173                    .entry(tag.to_string())
174                    .or_insert_with(Vec::new)
175                    .push((value.clone(), *pos));
176            } else if let Some(seq_c_start) = sequence_c_start_idx {
177                if i >= seq_c_start {
178                    // After sequence C start = Sequence C
179                    seq_c
180                        .entry(tag.to_string())
181                        .or_insert_with(Vec::new)
182                        .push((value.clone(), *pos));
183                } else {
184                    // Between sequence B start and C start = Sequence B
185                    seq_b
186                        .entry(tag.to_string())
187                        .or_insert_with(Vec::new)
188                        .push((value.clone(), *pos));
189                }
190            } else {
191                // No sequence C, everything after sequence B start is sequence B
192                seq_b
193                    .entry(tag.to_string())
194                    .or_insert_with(Vec::new)
195                    .push((value.clone(), *pos));
196            }
197        } else {
198            // No sequence B found, everything is sequence A
199            seq_a
200                .entry(tag.to_string())
201                .or_insert_with(Vec::new)
202                .push((value.clone(), *pos));
203        }
204    }
205
206    Ok(ParsedSequences {
207        sequence_a: seq_a,
208        sequence_b: seq_b,
209        sequence_c: seq_c,
210    })
211}
212
213/// Parse repetitive sequence items (like transactions)
214pub fn parse_repetitive_sequence<T>(fields: &FieldMap, marker_field: &str) -> Result<Vec<FieldMap>>
215where
216    T: crate::SwiftMessageBody,
217{
218    let mut items = Vec::new();
219
220    // Get all fields sorted by position
221    let mut all_fields: Vec<(String, String, usize)> = Vec::new();
222    for (tag, values) in fields {
223        for (value, pos) in values {
224            all_fields.push((tag.clone(), value.clone(), *pos));
225        }
226    }
227    all_fields.sort_by_key(|(_, _, pos)| *pos);
228
229    // Group fields by item (each starting with marker field)
230    let mut current_item_fields: HashMap<String, Vec<(String, usize)>> = HashMap::new();
231    let mut in_item = false;
232
233    for (tag, value, pos) in all_fields {
234        // Check if this is the start of a new item
235        if is_sequence_b_marker(&tag, marker_field) {
236            // Save previous item if exists
237            if in_item && !current_item_fields.is_empty() {
238                items.push(current_item_fields.clone());
239                current_item_fields.clear();
240            }
241            in_item = true;
242        }
243
244        // Add field to current item if we're in one
245        if in_item {
246            current_item_fields
247                .entry(tag)
248                .or_default()
249                .push((value, pos));
250        }
251    }
252
253    // Save the last item
254    if in_item && !current_item_fields.is_empty() {
255        items.push(current_item_fields);
256    }
257
258    Ok(items)
259}
260
261/// Check if a field tag is a sequence B marker
262fn is_sequence_b_marker(tag: &str, marker: &str) -> bool {
263    // Handle simple markers like "21"
264    if tag == marker {
265        return true;
266    }
267
268    // Handle numbered markers (e.g., "21" but not "21R", "21C", etc.)
269    if marker == "21" && tag == "21" {
270        return true;
271    }
272
273    false
274}
275
276/// Get sequence configuration for a specific message type
277pub fn get_sequence_config(message_type: &str) -> SequenceConfig {
278    match message_type {
279        "MT101" => SequenceConfig {
280            sequence_b_marker: "21".to_string(),
281            sequence_c_fields: vec![],
282            has_sequence_c: false,
283        },
284        "MT104" => SequenceConfig {
285            sequence_b_marker: "21".to_string(),
286            sequence_c_fields: vec![
287                "32B".to_string(),
288                "19".to_string(),
289                "71F".to_string(),
290                "71G".to_string(),
291                "53".to_string(),
292            ],
293            has_sequence_c: true,
294        },
295        "MT107" => SequenceConfig {
296            sequence_b_marker: "21".to_string(),
297            sequence_c_fields: vec![],
298            has_sequence_c: false,
299        },
300        _ => SequenceConfig::default(),
301    }
302}