swift_mt_message/
tokenizer.rs

1//! Block and field tokenizer for SWIFT MT messages
2//!
3//! This module handles the extraction of SWIFT message blocks and parsing of individual fields
4//! from Block 4 (Text Block). It provides efficient tokenization with proper error handling
5//! and supports streaming for large messages.
6
7use once_cell::sync::Lazy;
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10
11use crate::errors::{ParseError, Result};
12
13/// SWIFT message blocks structure
14#[derive(Default, Debug, Clone, Serialize, Deserialize)]
15pub struct SwiftMessageBlocks {
16    pub block_1: Option<String>,
17    pub block_2: Option<String>,
18    pub block_3: Option<String>,
19    pub block_4: Option<String>,
20    pub block_5: Option<String>,
21}
22
23/// Parsed field structure
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ParsedField {
26    pub tag: String,        // e.g., "50A", "20", "32A"
27    pub content: String,    // Raw field content
28    pub line_number: usize, // Line number for error reporting
29    pub column: usize,      // Column position for error reporting
30}
31
32/// Static regex patterns for field extraction
33static FIELD_REGEX: Lazy<Regex> =
34    Lazy::new(|| Regex::new(r":(\d{2}[A-Z]?):").expect("Invalid field regex"));
35
36/// Extract blocks from a SWIFT message using proper brace matching
37pub fn extract_blocks(message: &str) -> Result<SwiftMessageBlocks> {
38    let mut blocks = SwiftMessageBlocks::default();
39    let chars: Vec<char> = message.chars().collect();
40    let mut i = 0;
41
42    while i < chars.len() {
43        if chars[i] == '{' && i + 2 < chars.len() && chars[i + 2] == ':' {
44            // Found potential block start like {1:, {2:, etc.
45            let block_number = chars[i + 1];
46
47            if block_number.is_ascii_digit() {
48                // Find the matching closing brace using proper brace counting
49                let mut brace_count = 1;
50                let content_start = i + 3; // Skip "{N:"
51                let mut j = content_start;
52
53                while j < chars.len() && brace_count > 0 {
54                    match chars[j] {
55                        '{' => brace_count += 1,
56                        '}' => brace_count -= 1,
57                        _ => {}
58                    }
59                    j += 1;
60                }
61
62                if brace_count == 0 {
63                    // Found matching closing brace
64                    let content_end = j - 1; // Exclude the closing brace
65                    let block_content: String = chars[content_start..content_end].iter().collect();
66
67                    match block_number {
68                        '1' => blocks.block_1 = Some(block_content),
69                        '2' => blocks.block_2 = Some(block_content),
70                        '3' => blocks.block_3 = Some(block_content),
71                        '4' => blocks.block_4 = Some(block_content),
72                        '5' => blocks.block_5 = Some(block_content),
73                        _ => {
74                            return Err(ParseError::UnknownBlockNumber {
75                                block_number: block_number.to_string(),
76                                line: 1, // TODO: Calculate actual line number
77                                column: i + 1,
78                            });
79                        }
80                    }
81
82                    i = j; // Move past this block
83                } else {
84                    return Err(ParseError::InvalidBlockFormat {
85                        message: format!("Unmatched opening brace for block {}", block_number),
86                        line: 1,
87                        column: i + 1,
88                    });
89                }
90            } else {
91                i += 1;
92            }
93        } else {
94            i += 1;
95        }
96    }
97
98    if blocks.block_1.is_none() && blocks.block_2.is_none() && blocks.block_4.is_none() {
99        return Err(ParseError::NoBlocksFound {
100            message: "No valid SWIFT blocks found in message".to_string(),
101        });
102    }
103
104    Ok(blocks)
105}
106
107/// Parse Block 4 fields into individual field structures
108pub fn parse_block4_fields(block4_content: &str) -> Result<Vec<ParsedField>> {
109    if block4_content.is_empty() {
110        return Ok(Vec::new());
111    }
112
113    let mut fields = Vec::new();
114    let _current_line = 1usize;
115    let lines = block4_content.lines().enumerate();
116
117    // Find all field tags and their positions
118    let mut field_positions = Vec::new();
119
120    for (line_idx, line) in lines {
121        if let Some(captures) = FIELD_REGEX.captures(line) {
122            if let Some(tag_match) = captures.get(1) {
123                field_positions.push((
124                    tag_match.as_str().to_string(),
125                    line_idx + 1,
126                    tag_match.start(),
127                ));
128            }
129        }
130    }
131
132    if field_positions.is_empty() {
133        return Ok(Vec::new());
134    }
135
136    // Split content by field boundaries
137    let content_lines: Vec<&str> = block4_content.lines().collect();
138
139    for (i, (tag, line_number, column)) in field_positions.iter().enumerate() {
140        let start_line = line_number - 1; // Convert to 0-based
141        let end_line = if i + 1 < field_positions.len() {
142            field_positions[i + 1].1 - 1 // Next field's line - 1
143        } else {
144            content_lines.len() // End of content
145        };
146
147        // Extract field content
148        let mut field_content = String::new();
149
150        // First line: content after the tag
151        if start_line < content_lines.len() {
152            let first_line = content_lines[start_line];
153            if let Some(tag_end) = first_line.find(&format!(":{}:", tag)) {
154                let content_start = tag_end + tag.len() + 2; // Skip ":tag:"
155                if content_start < first_line.len() {
156                    field_content.push_str(&first_line[content_start..]);
157                }
158            }
159        }
160
161        // Subsequent lines until next field or end
162        for line_idx in (start_line + 1)..end_line {
163            if line_idx < content_lines.len() {
164                if !field_content.is_empty() {
165                    field_content.push('\n');
166                }
167                field_content.push_str(content_lines[line_idx]);
168            }
169        }
170
171        // Clean up field content - remove message terminator and trim
172        let cleaned_content = field_content
173            .trim()
174            .trim_end_matches('-') // Remove trailing dash (message terminator)
175            .trim() // Trim again after removing dash
176            .to_string();
177
178        fields.push(ParsedField {
179            tag: tag.clone(),
180            content: cleaned_content,
181            line_number: *line_number,
182            column: *column,
183        });
184    }
185
186    Ok(fields)
187}
188
189/// Extract message type from blocks
190pub fn extract_message_type(blocks: &SwiftMessageBlocks) -> Result<String> {
191    let block2 = blocks
192        .block_2
193        .as_ref()
194        .ok_or_else(|| ParseError::MissingRequiredBlock {
195            block: "2".to_string(),
196            message: "Block 2 is required to determine message type".to_string(),
197            line: 1,
198            column: 1,
199        })?;
200
201    if block2.len() < 4 {
202        return Err(ParseError::InvalidBlockFormat {
203            message: "Block 2 too short to contain message type".to_string(),
204            line: 1,
205            column: 1,
206        });
207    }
208
209    // Message type is characters 1-3 in block 2 (after I/O identifier)
210    Ok(block2[1..4].to_string())
211}
212
213/// Basic Header (Block 1) structure
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
215pub struct BasicHeader {
216    pub application_id: String,   // F = FIN, A = GPA, L = Login
217    pub service_id: String,       // 01 = FIN/GPA, 21 = ACK/NAK
218    pub logical_terminal: String, // 12 character LT address
219    pub session_number: String,   // 4 digit session number
220    pub sequence_number: String,  // 6 digit sequence number
221}
222
223/// Application Header (Block 2) structure
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225pub struct ApplicationHeader {
226    pub input_output_identifier: String,     // I = Input, O = Output
227    pub message_type: String,                // 3 digit message type
228    pub destination_address: String,         // 12 character destination LT
229    pub priority: String,                    // N = Normal, U = Urgent, S = System
230    pub delivery_monitoring: Option<String>, // 1 = Non-delivery notification
231    pub obsolescence_period: Option<String>, // 3 digit period
232}
233
234/// User Header (Block 3) structure based on SWIFT MT standards
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
236pub struct UserHeader {
237    /// Tag 103 - Service Identifier (3!a) - Mandatory for FINcopy Service
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub service_identifier: Option<String>,
240
241    /// Tag 113 - Banking Priority (4!x) - Optional
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub banking_priority: Option<String>,
244
245    /// Tag 108 - Message User Reference (16!x) - Optional
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub message_user_reference: Option<String>,
248
249    /// Tag 119 - Validation Flag (8c) - Optional (STP, REMIT, RFDD, COV)
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub validation_flag: Option<String>,
252
253    /// Tag 423 - Balance checkpoint date and time (YYMMDDHHMMSS[ss]) - Optional (MIRS only)
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub balance_checkpoint: Option<BalanceCheckpoint>,
256
257    /// Tag 106 - Message Input Reference MIR (28c) - Optional (MIRS only)
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub message_input_reference: Option<MessageInputReference>,
260
261    /// Tag 424 - Related reference (16x) - Optional (MIRS only)
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub related_reference: Option<String>,
264
265    /// Tag 111 - Service type identifier (3!n) - Optional
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub service_type_identifier: Option<String>,
268
269    /// Tag 121 - Unique end-to-end transaction reference (UUID format) - Mandatory for GPI
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub unique_end_to_end_reference: Option<String>,
272
273    /// Tag 115 - Addressee Information (32x) - Optional (FINCopy only)
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub addressee_information: Option<String>,
276
277    /// Tag 165 - Payment release information receiver (3!c/34x) - Optional (FINInform only)
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub payment_release_information: Option<PaymentReleaseInfo>,
280
281    /// Tag 433 - Sanctions screening information (3!a/[20x]) - Optional
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
284
285    /// Tag 434 - Payment controls information (3!a/[20x]) - Optional
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub payment_controls_info: Option<PaymentControlsInfo>,
288}
289
290/// Balance checkpoint structure for Tag 423
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
292pub struct BalanceCheckpoint {
293    pub date: String,                         // YYMMDD
294    pub time: String,                         // HHMMSS
295    pub hundredths_of_second: Option<String>, // ss (optional)
296}
297
298/// Message Input Reference structure for Tag 106
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
300pub struct MessageInputReference {
301    pub date: String,            // YYMMDD
302    pub lt_identifier: String,   // 12 characters
303    pub branch_code: String,     // 3!c
304    pub session_number: String,  // 4!n
305    pub sequence_number: String, // 6!n
306}
307
308/// Payment release information structure for Tag 165
309#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
310pub struct PaymentReleaseInfo {
311    pub code: String,                    // 3!c
312    pub additional_info: Option<String>, // 34x (optional)
313}
314
315/// Sanctions screening information structure for Tag 433
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub struct SanctionsScreeningInfo {
318    pub code_word: String,               // 3!a (AOK, FPO, NOK)
319    pub additional_info: Option<String>, // 20x (optional)
320}
321
322/// Payment controls information structure for Tag 434
323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
324pub struct PaymentControlsInfo {
325    pub code_word: String,               // 3!a
326    pub additional_info: Option<String>, // 20x (optional)
327}
328
329/// Trailer (Block 5) structure based on SWIFT MT standards
330#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
331pub struct Trailer {
332    /// CHK - Checksum (12!h) - Mandatory
333    pub checksum: String,
334
335    /// TNG - Test & Training Message - Optional (empty tag)
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub test_and_training: Option<bool>,
338
339    /// PDE - Possible Duplicate Emission - Optional
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
342
343    /// DLM - Delayed Message - Optional (empty tag)
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub delayed_message: Option<bool>,
346
347    /// MRF - Message Reference - Optional
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub message_reference: Option<MessageReference>,
350
351    /// PDM - Possible Duplicate Message - Optional
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
354
355    /// SYS - System Originated Message - Optional
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub system_originated_message: Option<SystemOriginatedMessage>,
358}
359
360/// Possible Duplicate Emission structure for PDE tag
361#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
362pub struct PossibleDuplicateEmission {
363    pub time: Option<String>, // HHMM (optional)
364    pub message_input_reference: Option<MessageInputReference>, // MIR (optional)
365}
366
367/// Message Reference structure for MRF tag
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
369pub struct MessageReference {
370    pub date: String,                                   // YYMMDD
371    pub full_time: String,                              // HHMM
372    pub message_input_reference: MessageInputReference, // MIR
373}
374
375/// Possible Duplicate Message structure for PDM tag
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
377pub struct PossibleDuplicateMessage {
378    pub time: Option<String>, // HHMM (optional)
379    pub message_output_reference: Option<MessageOutputReference>, // MOR (optional)
380}
381
382/// Message Output Reference structure (similar to MIR but for output)
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
384pub struct MessageOutputReference {
385    pub date: String,            // YYMMDD
386    pub lt_identifier: String,   // 12 characters
387    pub branch_code: String,     // 3!c
388    pub session_number: String,  // 4!n
389    pub sequence_number: String, // 6!n
390}
391
392/// System Originated Message structure for SYS tag
393#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
394pub struct SystemOriginatedMessage {
395    pub time: Option<String>, // HHMM (optional)
396    pub message_input_reference: Option<MessageInputReference>, // MIR (optional)
397}
398
399/// Parse basic header from Block 1
400pub fn parse_basic_header(block1_content: &str) -> Result<BasicHeader> {
401    if block1_content.len() < 21 {
402        return Err(ParseError::InvalidBlockFormat {
403            message: format!(
404                "Basic header block too short: expected at least 21 characters, got {}",
405                block1_content.len()
406            ),
407            line: 1,
408            column: 1,
409        });
410    }
411
412    Ok(BasicHeader {
413        application_id: block1_content[0..1].to_string(),
414        service_id: block1_content[1..3].to_string(),
415        logical_terminal: block1_content[3..15].to_string(),
416        session_number: block1_content[15..19].to_string(),
417        sequence_number: block1_content[19..].to_string(),
418    })
419}
420
421/// Parse application header from Block 2
422pub fn parse_application_header(block2_content: &str) -> Result<ApplicationHeader> {
423    if block2_content.is_empty() {
424        return Err(ParseError::InvalidBlockFormat {
425            message: "Application header block is empty".to_string(),
426            line: 1,
427            column: 1,
428        });
429    }
430
431    if block2_content.len() < 4 {
432        return Err(ParseError::InvalidBlockFormat {
433            message: format!(
434                "Application header block too short: expected at least 4 characters, got {}",
435                block2_content.len()
436            ),
437            line: 1,
438            column: 1,
439        });
440    }
441
442    let input_output_identifier = block2_content[0..1].to_string();
443    let message_type = block2_content[1..4].to_string();
444    let remaining = &block2_content[4..];
445
446    let (destination_address, priority, delivery_monitoring, obsolescence_period) =
447        if input_output_identifier == "I" {
448            // Input message format
449            if remaining.len() >= 12 {
450                let dest = remaining[0..12].to_string();
451                let prio = remaining
452                    .get(12..13)
453                    .map(|s| s.to_string())
454                    .unwrap_or_default();
455                let del_mon = remaining.get(13..14).map(|s| s.to_string());
456                let obs_per = remaining.get(14..17).map(|s| s.to_string());
457                (dest, prio, del_mon, obs_per)
458            } else {
459                (String::new(), String::new(), None, None)
460            }
461        } else if input_output_identifier == "O" {
462            // Output message format - different structure
463            (String::new(), String::new(), None, None) // TODO: Implement output message parsing
464        } else {
465            return Err(ParseError::InvalidBlockFormat {
466                message: format!("Invalid I/O identifier: {}", input_output_identifier),
467                line: 1,
468                column: 1,
469            });
470        };
471
472    Ok(ApplicationHeader {
473        input_output_identifier,
474        message_type,
475        destination_address,
476        priority,
477        delivery_monitoring,
478        obsolescence_period,
479    })
480}
481
482/// Parse user header from Block 3 using structured parsing
483pub fn parse_user_header(block3_content: &str) -> Result<UserHeader> {
484    let mut user_header = UserHeader::default();
485
486    // Parse nested tags in format {tag:value}
487    let tag_value_regex =
488        Regex::new(r"\{([^:]+):([^}]*)\}").map_err(|e| ParseError::RegexError {
489            message: e.to_string(),
490        })?;
491
492    for cap in tag_value_regex.captures_iter(block3_content) {
493        if let (Some(tag), Some(value)) = (cap.get(1), cap.get(2)) {
494            let tag_str = tag.as_str();
495            let value_str = value.as_str();
496
497            match tag_str {
498                "103" => user_header.service_identifier = Some(value_str.to_string()),
499                "113" => user_header.banking_priority = Some(value_str.to_string()),
500                "108" => user_header.message_user_reference = Some(value_str.to_string()),
501                "119" => user_header.validation_flag = Some(value_str.to_string()),
502                "423" => user_header.balance_checkpoint = parse_balance_checkpoint(value_str)?,
503                "106" => {
504                    user_header.message_input_reference = parse_message_input_reference(value_str)?
505                }
506                "424" => user_header.related_reference = Some(value_str.to_string()),
507                "111" => user_header.service_type_identifier = Some(value_str.to_string()),
508                "121" => user_header.unique_end_to_end_reference = Some(value_str.to_string()),
509                "115" => user_header.addressee_information = Some(value_str.to_string()),
510                "165" => {
511                    user_header.payment_release_information = parse_payment_release_info(value_str)?
512                }
513                "433" => {
514                    user_header.sanctions_screening_info =
515                        parse_sanctions_screening_info(value_str)?
516                }
517                "434" => {
518                    user_header.payment_controls_info = parse_payment_controls_info(value_str)?
519                }
520                _ => {
521                    // Unknown tag - could log a warning but continue parsing
522                }
523            }
524        }
525    }
526
527    Ok(user_header)
528}
529
530/// Parse trailer block from Block 5 using structured parsing
531pub fn parse_trailer_block(block5_content: &str) -> Result<Trailer> {
532    let mut trailer = Trailer {
533        checksum: String::new(), // Will be set below
534        ..Default::default()
535    };
536
537    // Parse nested tags in format {tag:value}
538    let tag_value_regex =
539        Regex::new(r"\{([^:]+):([^}]*)\}").map_err(|e| ParseError::RegexError {
540            message: e.to_string(),
541        })?;
542
543    for cap in tag_value_regex.captures_iter(block5_content) {
544        if let (Some(tag), Some(value)) = (cap.get(1), cap.get(2)) {
545            let tag_str = tag.as_str();
546            let value_str = value.as_str();
547
548            match tag_str {
549                "CHK" => trailer.checksum = value_str.to_string(),
550                "TNG" => trailer.test_and_training = Some(true),
551                "PDE" => {
552                    trailer.possible_duplicate_emission =
553                        parse_possible_duplicate_emission(value_str)?
554                }
555                "DLM" => trailer.delayed_message = Some(true),
556                "MRF" => trailer.message_reference = parse_message_reference(value_str)?,
557                "PDM" => {
558                    trailer.possible_duplicate_message =
559                        parse_possible_duplicate_message(value_str)?
560                }
561                "SYS" => {
562                    trailer.system_originated_message = parse_system_originated_message(value_str)?
563                }
564                _ => {
565                    // Unknown tag - could log a warning but continue parsing
566                }
567            }
568        }
569    }
570
571    Ok(trailer)
572}
573
574/// Parse balance checkpoint from Tag 423 value
575fn parse_balance_checkpoint(value: &str) -> Result<Option<BalanceCheckpoint>> {
576    if value.is_empty() {
577        return Ok(None);
578    }
579
580    if value.len() < 12 {
581        return Ok(None); // Invalid format
582    }
583
584    let date = value[0..6].to_string();
585    let time = value[6..12].to_string();
586    let hundredths = if value.len() > 12 {
587        Some(value[12..].to_string())
588    } else {
589        None
590    };
591
592    Ok(Some(BalanceCheckpoint {
593        date,
594        time,
595        hundredths_of_second: hundredths,
596    }))
597}
598
599/// Parse Message Input Reference from Tag 106 value
600fn parse_message_input_reference(value: &str) -> Result<Option<MessageInputReference>> {
601    if value.is_empty() || value.len() < 28 {
602        return Ok(None); // Invalid MIR format
603    }
604
605    Ok(Some(MessageInputReference {
606        date: value[0..6].to_string(),
607        lt_identifier: value[6..18].to_string(),
608        branch_code: value[18..21].to_string(),
609        session_number: value[21..25].to_string(),
610        sequence_number: value[25..].to_string(),
611    }))
612}
613
614/// Parse payment release information from Tag 165 value
615fn parse_payment_release_info(value: &str) -> Result<Option<PaymentReleaseInfo>> {
616    if value.is_empty() {
617        return Ok(None);
618    }
619
620    // Format: /3!c/[34x]
621    if value.starts_with('/') && value.len() >= 4 {
622        let parts: Vec<&str> = value[1..].split('/').collect();
623        if !parts.is_empty() {
624            let code = parts[0].to_string();
625            let additional_info = if parts.len() > 1 && !parts[1].is_empty() {
626                Some(parts[1].to_string())
627            } else {
628                None
629            };
630            return Ok(Some(PaymentReleaseInfo {
631                code,
632                additional_info,
633            }));
634        }
635    }
636
637    Ok(None)
638}
639
640/// Parse sanctions screening information from Tag 433 value
641fn parse_sanctions_screening_info(value: &str) -> Result<Option<SanctionsScreeningInfo>> {
642    if value.is_empty() {
643        return Ok(None);
644    }
645
646    // Format: /3!a/[20x]
647    if value.starts_with('/') && value.len() >= 4 {
648        let parts: Vec<&str> = value[1..].split('/').collect();
649        if !parts.is_empty() {
650            let code_word = parts[0].to_string();
651            let additional_info = if parts.len() > 1 && !parts[1].is_empty() {
652                Some(parts[1].to_string())
653            } else {
654                None
655            };
656            return Ok(Some(SanctionsScreeningInfo {
657                code_word,
658                additional_info,
659            }));
660        }
661    }
662
663    Ok(None)
664}
665
666/// Parse payment controls information from Tag 434 value
667fn parse_payment_controls_info(value: &str) -> Result<Option<PaymentControlsInfo>> {
668    if value.is_empty() {
669        return Ok(None);
670    }
671
672    // Format: /3!a/[20x]
673    if value.starts_with('/') && value.len() >= 4 {
674        let parts: Vec<&str> = value[1..].split('/').collect();
675        if !parts.is_empty() {
676            let code_word = parts[0].to_string();
677            let additional_info = if parts.len() > 1 && !parts[1].is_empty() {
678                Some(parts[1].to_string())
679            } else {
680                None
681            };
682            return Ok(Some(PaymentControlsInfo {
683                code_word,
684                additional_info,
685            }));
686        }
687    }
688
689    Ok(None)
690}
691
692/// Parse possible duplicate emission from PDE tag value
693fn parse_possible_duplicate_emission(value: &str) -> Result<Option<PossibleDuplicateEmission>> {
694    if value.is_empty() {
695        return Ok(Some(PossibleDuplicateEmission {
696            time: None,
697            message_input_reference: None,
698        }));
699    }
700
701    // Format can be: time+MIR, time only, MIR only, or empty
702    let time = if value.len() >= 4 {
703        Some(value[0..4].to_string())
704    } else {
705        None
706    };
707
708    let mir = if value.len() > 4 {
709        parse_message_input_reference(&value[4..])?
710    } else {
711        None
712    };
713
714    Ok(Some(PossibleDuplicateEmission {
715        time,
716        message_input_reference: mir,
717    }))
718}
719
720/// Parse message reference from MRF tag value
721fn parse_message_reference(value: &str) -> Result<Option<MessageReference>> {
722    if value.is_empty() || value.len() < 38 {
723        return Ok(None); // Invalid format
724    }
725
726    let date = value[0..6].to_string();
727    let full_time = value[6..10].to_string();
728    let mir_part = &value[10..];
729
730    let mir =
731        parse_message_input_reference(mir_part)?.ok_or_else(|| ParseError::InvalidBlockFormat {
732            message: "Invalid MIR in MRF tag".to_string(),
733            line: 1,
734            column: 1,
735        })?;
736
737    Ok(Some(MessageReference {
738        date,
739        full_time,
740        message_input_reference: mir,
741    }))
742}
743
744/// Parse possible duplicate message from PDM tag value
745fn parse_possible_duplicate_message(value: &str) -> Result<Option<PossibleDuplicateMessage>> {
746    if value.is_empty() {
747        return Ok(Some(PossibleDuplicateMessage {
748            time: None,
749            message_output_reference: None,
750        }));
751    }
752
753    // Format similar to PDE but with MOR instead of MIR
754    let time = if value.len() >= 4 {
755        Some(value[0..4].to_string())
756    } else {
757        None
758    };
759
760    let mor = if value.len() > 4 {
761        parse_message_output_reference(&value[4..])?
762    } else {
763        None
764    };
765
766    Ok(Some(PossibleDuplicateMessage {
767        time,
768        message_output_reference: mor,
769    }))
770}
771
772/// Parse message output reference (similar to MIR)
773fn parse_message_output_reference(value: &str) -> Result<Option<MessageOutputReference>> {
774    if value.is_empty() || value.len() < 28 {
775        return Ok(None); // Invalid MOR format
776    }
777
778    Ok(Some(MessageOutputReference {
779        date: value[0..6].to_string(),
780        lt_identifier: value[6..18].to_string(),
781        branch_code: value[18..21].to_string(),
782        session_number: value[21..25].to_string(),
783        sequence_number: value[25..].to_string(),
784    }))
785}
786
787/// Parse system originated message from SYS tag value
788fn parse_system_originated_message(value: &str) -> Result<Option<SystemOriginatedMessage>> {
789    if value.is_empty() {
790        return Ok(Some(SystemOriginatedMessage {
791            time: None,
792            message_input_reference: None,
793        }));
794    }
795
796    // Format similar to PDE
797    let time = if value.len() >= 4 {
798        Some(value[0..4].to_string())
799    } else {
800        None
801    };
802
803    let mir = if value.len() > 4 {
804        parse_message_input_reference(&value[4..])?
805    } else {
806        None
807    };
808
809    Ok(Some(SystemOriginatedMessage {
810        time,
811        message_input_reference: mir,
812    }))
813}
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818
819    #[test]
820    fn test_extract_blocks() {
821        let message = "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{4:\n:20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n-}";
822
823        let blocks = extract_blocks(message).unwrap();
824
825        assert!(blocks.block_1.is_some());
826        assert_eq!(blocks.block_1.unwrap(), "F01BANKDEFFAXXX0123456789");
827
828        assert!(blocks.block_2.is_some());
829        assert_eq!(blocks.block_2.unwrap(), "I103BANKDEFFAXXXU3003");
830
831        assert!(blocks.block_4.is_some());
832        assert!(blocks.block_4.unwrap().contains(":20:FT21234567890"));
833    }
834
835    #[test]
836    fn test_extract_blocks_with_nested_braces() {
837        // Test message with Block 3 containing nested braces (user header)
838        let message = "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{3:{113:SEPA}{108:MT103}{119:STP}}{4:\n:20:FT21234567890\n:23B:CRED\n-}{5:{MAC:12345678}{CHK:ABCDEF}}";
839
840        let blocks = extract_blocks(message).unwrap();
841
842        assert!(blocks.block_1.is_some());
843        assert_eq!(blocks.block_1.unwrap(), "F01BANKDEFFAXXX0123456789");
844
845        assert!(blocks.block_2.is_some());
846        assert_eq!(blocks.block_2.unwrap(), "I103BANKDEFFAXXXU3003");
847
848        assert!(blocks.block_3.is_some());
849        assert_eq!(blocks.block_3.unwrap(), "{113:SEPA}{108:MT103}{119:STP}");
850
851        assert!(blocks.block_4.is_some());
852        assert!(blocks.block_4.unwrap().contains(":20:FT21234567890"));
853
854        assert!(blocks.block_5.is_some());
855        assert_eq!(blocks.block_5.unwrap(), "{MAC:12345678}{CHK:ABCDEF}");
856    }
857
858    #[test]
859    fn test_extract_blocks_single_nested_brace() {
860        // Test with just one nested tag in Block 3
861        let message =
862            "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{3:{113:SEPA}}{4:\n:20:TEST\n-}";
863
864        let blocks = extract_blocks(message).unwrap();
865
866        assert!(blocks.block_3.is_some());
867        assert_eq!(blocks.block_3.unwrap(), "{113:SEPA}");
868    }
869
870    #[test]
871    fn test_parse_block4_fields() {
872        let block4 = ":20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n:50K:JOHN DOE\nACME CORP\n123 MAIN ST\n:59:JANE SMITH\nXYZ COMPANY";
873
874        let fields = parse_block4_fields(block4).unwrap();
875
876        assert_eq!(fields.len(), 5);
877        assert_eq!(fields[0].tag, "20");
878        assert_eq!(fields[0].content, "FT21234567890");
879        assert_eq!(fields[1].tag, "23B");
880        assert_eq!(fields[1].content, "CRED");
881        assert_eq!(fields[3].tag, "50K");
882        assert_eq!(fields[3].content, "JOHN DOE\nACME CORP\n123 MAIN ST");
883    }
884
885    #[test]
886    fn test_extract_message_type() {
887        let blocks = SwiftMessageBlocks {
888            block_1: Some("F01BANKDEFFAXXX0123456789".to_string()),
889            block_2: Some("I103BANKDEFFAXXXU3003".to_string()),
890            block_3: None,
891            block_4: Some(":20:TEST".to_string()),
892            block_5: None,
893        };
894
895        let message_type = extract_message_type(&blocks).unwrap();
896        assert_eq!(message_type, "103");
897    }
898
899    #[test]
900    fn test_parse_basic_header() {
901        let block1 = "F01BANKDEFFAXXX0123456789";
902        let header = parse_basic_header(block1).unwrap();
903
904        assert_eq!(header.application_id, "F");
905        assert_eq!(header.service_id, "01");
906        assert_eq!(header.logical_terminal, "BANKDEFFAXXX");
907        assert_eq!(header.session_number, "0123");
908        assert_eq!(header.sequence_number, "456789");
909    }
910
911    #[test]
912    fn test_parse_application_header() {
913        let block2 = "I103BANKDEFFAXXXU3003";
914        let header = parse_application_header(block2).unwrap();
915
916        assert_eq!(header.input_output_identifier, "I");
917        assert_eq!(header.message_type, "103");
918        assert_eq!(header.destination_address, "BANKDEFFAXXX");
919        assert_eq!(header.priority, "U");
920    }
921}