swift_mt_message/headers/
mod.rs

1//! # SWIFT Message Headers and Trailers
2//!
3//! SWIFT FIN block structures for MT message headers and trailers.
4//!
5//! ## Block Structure
6//! - **Block 1:** Basic Header - Sender identification and routing
7//! - **Block 2:** Application Header - Message type and delivery info
8//! - **Block 3:** User Header - Optional service tags (UETR, validation flags)
9//! - **Block 5:** Trailer - Security (MAC, checksum) and control tags
10//!
11//! ## Usage
12//! ```rust
13//! use swift_mt_message::headers::{BasicHeader, ApplicationHeader};
14//!
15//! # fn main() -> swift_mt_message::Result<()> {
16//! let basic = BasicHeader::parse("F01DEUTDEFFAXXX0000123456")?;
17//! let app = ApplicationHeader::parse("I103CHASUS33AXXXN")?;
18//! # Ok(())
19//! # }
20//! ```
21
22use crate::errors::{ParseError, Result};
23use serde::{Deserialize, Serialize};
24
25/// **Block 1: Basic Header**
26///
27/// Sender identification and message routing information.
28///
29/// **Format:** `F01SSSSSSSSSCCC0000NNNNNN` (25 chars)
30/// - App ID (1): F=FIN, A=GPA
31/// - Service (2): 01=FIN, 03=FIN Copy
32/// - LT Address (12): BIC + terminal + branch
33/// - Session (4): 0000-9999
34/// - Sequence (6): 000000-999999
35///
36/// **Example:** `F01DEUTDEFFAXXX0000123456`
37#[derive(Debug, Clone, PartialEq)]
38pub struct BasicHeader {
39    /// Application ID (F, A, L)
40    pub application_id: String,
41    /// Service ID (01, 03, 05, 21)
42    pub service_id: String,
43    /// Logical terminal address (12 chars)
44    pub logical_terminal: String,
45    /// Sender BIC (8 chars)
46    pub sender_bic: String,
47    /// Session number (4 digits)
48    pub session_number: String,
49    /// Sequence number (6 digits)
50    pub sequence_number: String,
51}
52
53// Custom Serialize/Deserialize to normalize BIC padding
54impl serde::Serialize for BasicHeader {
55    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
56    where
57        S: serde::Serializer,
58    {
59        use serde::ser::SerializeStruct;
60
61        // Normalize logical_terminal to exactly 12 characters for JSON
62        let normalized_logical_terminal = if self.logical_terminal.len() > 12 {
63            self.logical_terminal[..12].to_string()
64        } else if self.logical_terminal.len() < 12 {
65            format!("{:X<12}", self.logical_terminal)
66        } else {
67            self.logical_terminal.clone()
68        };
69
70        let mut state = serializer.serialize_struct("BasicHeader", 5)?;
71        state.serialize_field("application_id", &self.application_id)?;
72        state.serialize_field("service_id", &self.service_id)?;
73        state.serialize_field("logical_terminal", &normalized_logical_terminal)?;
74        state.serialize_field("sender_bic", &self.sender_bic)?;
75        state.serialize_field("session_number", &self.session_number)?;
76        state.serialize_field("sequence_number", &self.sequence_number)?;
77        state.end()
78    }
79}
80
81impl<'de> serde::Deserialize<'de> for BasicHeader {
82    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
83    where
84        D: serde::Deserializer<'de>,
85    {
86        #[derive(Deserialize)]
87        struct BasicHeaderHelper {
88            application_id: String,
89            service_id: String,
90            logical_terminal: String,
91            sender_bic: String,
92            session_number: String,
93            sequence_number: String,
94        }
95
96        let helper = BasicHeaderHelper::deserialize(deserializer)?;
97
98        // Normalize logical_terminal to exactly 12 characters
99        let normalized_logical_terminal = if helper.logical_terminal.len() > 12 {
100            helper.logical_terminal[..12].to_string()
101        } else if helper.logical_terminal.len() < 12 {
102            format!("{:X<12}", helper.logical_terminal)
103        } else {
104            helper.logical_terminal.clone()
105        };
106
107        // Use the original sender_bic from the JSON
108        // It should match what's in the first part of logical_terminal
109        let sender_bic = helper.sender_bic;
110
111        Ok(BasicHeader {
112            application_id: helper.application_id,
113            service_id: helper.service_id,
114            logical_terminal: normalized_logical_terminal,
115            sender_bic,
116            session_number: helper.session_number,
117            sequence_number: helper.sequence_number,
118        })
119    }
120}
121
122impl BasicHeader {
123    /// Parse basic header from block 1 string
124    pub fn parse(block1: &str) -> Result<Self> {
125        // Expected format: F01SSSSSSSSSCCC0000NNNNNN (exactly 25 characters)
126        // Where: F=app_id, 01=service_id, SSSSSSSSSCCC=logical_terminal(12), 0000=session(4), NNNNNN=sequence(6)
127        if block1.len() != 25 {
128            return Err(ParseError::InvalidBlockStructure {
129                block: "1".to_string(),
130                message: format!(
131                    "Block 1 must be exactly 25 characters, got {}",
132                    block1.len()
133                ),
134            });
135        }
136
137        let application_id = block1[0..1].to_string();
138        let service_id = block1[1..3].to_string();
139        let raw_logical_terminal = block1[3..15].to_string();
140        let session_number = block1[15..19].to_string();
141        let sequence_number = block1[19..25].to_string();
142
143        // Keep the full 12-character logical terminal as stored in the MT format
144        // The padding is necessary for the MT format and we handle normalization in tests
145        let logical_terminal = raw_logical_terminal;
146
147        // Extract BIC from logical_terminal
148        // SWIFT BICs are either 8 or 11 characters
149        // The logical_terminal is 12 chars: BIC (8 or 11) + optional terminal ID
150        // Common patterns:
151        // - 8-char BIC + 4-char terminal: "DEUTDEFFAXXX" -> BIC="DEUTDEFF"
152        // - 11-char BIC + 1-char terminal: "DEUTDEFF001A" -> BIC="DEUTDEFF001"
153
154        let sender_bic = if logical_terminal.len() == 12 {
155            // Check if this looks like an 8-char BIC with terminal suffix
156            // Terminal suffixes are typically single letter + XXX (e.g., AXXX, BXXX)
157            // or just XXX for no specific terminal
158            let last_four = &logical_terminal[8..12];
159            if last_four == "XXXX" || (last_four.len() == 4 && &last_four[1..] == "XXX") {
160                // 8-character BIC with terminal suffix
161                logical_terminal[0..8].to_string()
162            } else {
163                // Check if chars 9-11 could be a valid branch code
164                let potential_branch = &logical_terminal[8..11];
165                if potential_branch.chars().all(|c| c.is_ascii_alphanumeric())
166                    && potential_branch != "XXX"
167                {
168                    // Likely an 11-character BIC
169                    logical_terminal[0..11].to_string()
170                } else {
171                    // Default to 8-character BIC
172                    logical_terminal[0..8].to_string()
173                }
174            }
175        } else if logical_terminal.len() >= 11 {
176            logical_terminal[0..11].to_string()
177        } else if logical_terminal.len() >= 8 {
178            logical_terminal[0..8].to_string()
179        } else {
180            // Should not happen with valid SWIFT messages
181            logical_terminal.clone()
182        };
183
184        Ok(BasicHeader {
185            application_id,
186            service_id,
187            logical_terminal,
188            sender_bic,
189            session_number,
190            sequence_number,
191        })
192    }
193}
194
195impl std::fmt::Display for BasicHeader {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        // Ensure consistent formatting:
198        // - logical_terminal must be exactly 12 characters (8-char BIC + 1-char LT + 3-char branch)
199        // - session_number must be exactly 4 digits
200        // - sequence_number must be exactly 6 digits
201
202        // Pad or truncate logical_terminal to exactly 12 characters
203        let logical_terminal = if self.logical_terminal.len() > 12 {
204            self.logical_terminal[..12].to_string()
205        } else if self.logical_terminal.len() < 12 {
206            // Pad with 'X' to reach 12 characters (standard for missing branch codes)
207            format!("{:X<12}", self.logical_terminal)
208        } else {
209            self.logical_terminal.clone()
210        };
211
212        // Ensure session_number is exactly 4 digits, left-padded with zeros
213        let session_number = format!(
214            "{:0>4}",
215            &self.session_number[..self.session_number.len().min(4)]
216        );
217
218        // Ensure sequence_number is exactly 6 digits, left-padded with zeros
219        let sequence_number = format!(
220            "{:0>6}",
221            &self.sequence_number[..self.sequence_number.len().min(6)]
222        );
223
224        write!(
225            f,
226            "{}{}{}{}{}",
227            self.application_id, self.service_id, logical_terminal, session_number, sequence_number
228        )
229    }
230}
231
232/// **Input Application Header**
233///
234/// Message being sent to SWIFT network.
235///
236/// **Format:** `I103DDDDDDDDDDDDP[M][OOO]` (17-21 chars)
237#[derive(Debug, Clone, PartialEq)]
238pub struct InputApplicationHeader {
239    /// Message type (3 digits: 103, 202, 940, etc.)
240    pub message_type: String,
241    /// Destination address (12 chars: BIC + terminal + branch)
242    pub destination_address: String,
243    /// Receiver BIC (8 chars)
244    pub receiver_bic: String,
245    /// Priority (U=Urgent, N=Normal, S=System)
246    pub priority: String,
247    /// Delivery monitoring (1, 2, 3)
248    pub delivery_monitoring: Option<String>,
249    /// Obsolescence period (003-999, units of 5 min)
250    pub obsolescence_period: Option<String>,
251}
252
253// Custom Serialize/Deserialize to normalize destination_address BIC padding
254impl serde::Serialize for InputApplicationHeader {
255    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
256    where
257        S: serde::Serializer,
258    {
259        use serde::ser::SerializeStruct;
260
261        // Normalize destination_address to exactly 12 characters for JSON
262        let normalized_destination_address = if self.destination_address.len() > 12 {
263            self.destination_address[..12].to_string()
264        } else if self.destination_address.len() < 12 {
265            format!("{:X<12}", self.destination_address)
266        } else {
267            self.destination_address.clone()
268        };
269
270        let field_count = 4
271            + self.delivery_monitoring.is_some() as usize
272            + self.obsolescence_period.is_some() as usize;
273        let mut state = serializer.serialize_struct("InputApplicationHeader", field_count)?;
274        state.serialize_field("message_type", &self.message_type)?;
275        state.serialize_field("destination_address", &normalized_destination_address)?;
276        state.serialize_field("receiver_bic", &self.receiver_bic)?;
277        state.serialize_field("priority", &self.priority)?;
278        if let Some(ref dm) = self.delivery_monitoring {
279            state.serialize_field("delivery_monitoring", dm)?;
280        }
281        if let Some(ref op) = self.obsolescence_period {
282            state.serialize_field("obsolescence_period", op)?;
283        }
284        state.end()
285    }
286}
287
288impl<'de> serde::Deserialize<'de> for InputApplicationHeader {
289    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
290    where
291        D: serde::Deserializer<'de>,
292    {
293        #[derive(Deserialize)]
294        struct InputApplicationHeaderHelper {
295            message_type: String,
296            destination_address: String,
297            receiver_bic: String,
298            priority: String,
299            delivery_monitoring: Option<String>,
300            obsolescence_period: Option<String>,
301        }
302
303        let helper = InputApplicationHeaderHelper::deserialize(deserializer)?;
304
305        // Normalize destination_address to exactly 12 characters
306        let normalized_destination_address = if helper.destination_address.len() > 12 {
307            helper.destination_address[..12].to_string()
308        } else if helper.destination_address.len() < 12 {
309            format!("{:X<12}", helper.destination_address)
310        } else {
311            helper.destination_address.clone()
312        };
313
314        // Use the original receiver_bic from the JSON
315        // It should match what's in the first part of destination_address
316        let receiver_bic = helper.receiver_bic;
317
318        Ok(InputApplicationHeader {
319            message_type: helper.message_type,
320            destination_address: normalized_destination_address,
321            receiver_bic,
322            priority: helper.priority,
323            delivery_monitoring: helper.delivery_monitoring,
324            obsolescence_period: helper.obsolescence_period,
325        })
326    }
327}
328
329/// **Output Application Header**
330///
331/// Message delivered from SWIFT network.
332///
333/// **Format:** `O103HHMMYYYYMMDDDDDDDDDDDDDDNNNNSSSSSSYYYYMMDDHHMMP` (46-47 chars)
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
335pub struct OutputApplicationHeader {
336    /// Message type (3 digits)
337    pub message_type: String,
338    /// Input time (HHMM)
339    pub input_time: String,
340    /// Message Input Reference (MIR)
341    pub mir: MessageInputReference,
342    /// Output date (YYMMDD)
343    pub output_date: String,
344    /// Output time (HHMM)
345    pub output_time: String,
346    /// Priority (U, N, S)
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub priority: Option<String>,
349}
350
351/// **Block 2: Application Header**
352///
353/// Message type and routing information.
354/// Direction-dependent: Input (I) for outgoing, Output (O) for incoming messages.
355///
356/// **Variants:**
357/// - **Input:** Message being sent to SWIFT (format: `I103DDDDDDDDDDDDP[M][OOO]`)
358/// - **Output:** Message delivered from SWIFT (format: `O103HHMMYYYYMMDDDDDDDDDDDDDDNNNNSSSSSSYYYYMMDDHHMMP`)
359#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
360#[serde(tag = "direction")]
361pub enum ApplicationHeader {
362    /// Input message sent to SWIFT network
363    #[serde(rename = "I")]
364    Input(InputApplicationHeader),
365
366    /// Output message delivered from SWIFT network
367    #[serde(rename = "O")]
368    Output(OutputApplicationHeader),
369}
370
371impl ApplicationHeader {
372    /// Parse application header from block 2 string
373    pub fn parse(block2: &str) -> Result<Self> {
374        if block2.len() < 4 {
375            return Err(ParseError::InvalidBlockStructure {
376                block: "2".to_string(),
377                message: format!(
378                    "Block 2 too short: expected at least 4 characters, got {}",
379                    block2.len()
380                ),
381            });
382        }
383
384        let direction = &block2[0..1];
385        let message_type = block2[1..4].to_string();
386
387        match direction {
388            "I" => {
389                // Input format: {2:I103DDDDDDDDDDDDP[M][OOO]}
390                // I + 103 + 12-char destination + priority + optional monitoring + optional obsolescence
391                if block2.len() < 17 {
392                    return Err(ParseError::InvalidBlockStructure {
393                        block: "2".to_string(),
394                        message: format!(
395                            "Input Block 2 too short: expected at least 17 characters, got {}",
396                            block2.len()
397                        ),
398                    });
399                }
400
401                let raw_destination_address = block2[4..16].to_string();
402                let priority = block2[16..17].to_string();
403
404                // Keep the full 12-character destination address as stored in the MT format
405                // The padding is necessary for the MT format and we handle normalization in tests
406                let destination_address = raw_destination_address;
407
408                // Extract BIC from destination_address
409                // SWIFT BICs are either 8 or 11 characters
410                // If the destination_address ends with XXX or XXXX, it's likely padding
411                let receiver_bic = if destination_address.len() == 12 {
412                    // Check if this looks like an 8-char BIC with terminal suffix
413                    // Terminal suffixes are typically single letter + XXX (e.g., AXXX, BXXX)
414                    // or just XXXX for no specific terminal
415                    let last_four = &destination_address[8..12];
416                    if last_four == "XXXX" || (last_four.len() == 4 && &last_four[1..] == "XXX") {
417                        // 8-character BIC with terminal suffix
418                        destination_address[0..8].to_string()
419                    } else {
420                        // Check if chars 9-11 could be a valid branch code
421                        let potential_branch = &destination_address[8..11];
422                        if potential_branch.chars().all(|c| c.is_ascii_alphanumeric())
423                            && potential_branch != "XXX"
424                        {
425                            // Likely an 11-character BIC
426                            destination_address[0..11].to_string()
427                        } else {
428                            // Default to 8-character BIC
429                            destination_address[0..8].to_string()
430                        }
431                    }
432                } else if destination_address.len() >= 11 {
433                    destination_address[0..11].to_string()
434                } else if destination_address.len() >= 8 {
435                    destination_address[0..8].to_string()
436                } else {
437                    // Should not happen with valid SWIFT messages
438                    destination_address.clone()
439                };
440
441                // Only parse delivery_monitoring if explicitly present
442                let delivery_monitoring = if block2.len() >= 18 {
443                    let monitoring = &block2[17..18];
444                    // Only set if it's a valid monitoring code (not a space or other character)
445                    if monitoring
446                        .chars()
447                        .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit())
448                    {
449                        Some(monitoring.to_string())
450                    } else {
451                        None
452                    }
453                } else {
454                    None
455                };
456
457                // Only parse obsolescence_period if explicitly present and delivery_monitoring exists
458                let obsolescence_period = if delivery_monitoring.is_some() && block2.len() >= 21 {
459                    Some(block2[18..21].to_string())
460                } else {
461                    None
462                };
463
464                Ok(ApplicationHeader::Input(InputApplicationHeader {
465                    message_type,
466                    destination_address,
467                    receiver_bic,
468                    priority,
469                    delivery_monitoring,
470                    obsolescence_period,
471                }))
472            }
473            "O" => {
474                // Output format: {2:O103HHMMYYYYMMDDDDDDDDDDDDDDNNNNSSSSSSYYYYMMDDHHMMP}
475                // Components:
476                // O (1) + message_type (3) + input_time (4) + mir (28) + output_date (6) + output_time (4) + priority (1)
477                // MIR consists of: date (6) + lt_address (12) + session (4) + sequence (6) = 28 chars
478                // Total: 1 + 3 + 4 + 28 + 6 + 4 = 46 characters minimum (priority optional)
479
480                if block2.len() < 46 {
481                    return Err(ParseError::InvalidBlockStructure {
482                        block: "2".to_string(),
483                        message: format!(
484                            "Output Block 2 too short: expected at least 46 characters, got {}",
485                            block2.len()
486                        ),
487                    });
488                }
489
490                // Parse Output format components according to SWIFT specification:
491                let input_time = block2[4..8].to_string(); // HHMM
492
493                // MIR (Message Input Reference) components
494                let mir_date = block2[8..14].to_string(); // YYMMDD
495                let mir_lt_address = block2[14..26].to_string(); // 12 characters (BIC8 + LT + Branch)
496                let mir_session = block2[26..30].to_string(); // 4 digits
497                let mir_sequence = block2[30..36].to_string(); // 6 digits
498
499                let output_date = block2[36..42].to_string(); // YYMMDD
500                let output_time = block2[42..46].to_string(); // HHMM
501
502                let priority = if block2.len() >= 47 {
503                    Some(block2[46..47].to_string())
504                } else {
505                    None
506                };
507
508                // Create MIR structure
509                let mir = MessageInputReference {
510                    date: mir_date,
511                    lt_identifier: mir_lt_address.clone(),
512                    branch_code: if mir_lt_address.len() >= 12 {
513                        mir_lt_address[9..12].to_string()
514                    } else {
515                        "XXX".to_string()
516                    },
517                    session_number: mir_session,
518                    sequence_number: mir_sequence,
519                };
520
521                Ok(ApplicationHeader::Output(OutputApplicationHeader {
522                    message_type,
523                    input_time,
524                    mir,
525                    output_date,
526                    output_time,
527                    priority,
528                }))
529            }
530            _ => Err(ParseError::InvalidBlockStructure {
531                block: "2".to_string(),
532                message: format!(
533                    "Invalid direction indicator: expected 'I' or 'O', got '{}'",
534                    direction
535                ),
536            }),
537        }
538    }
539
540    /// Get the message type regardless of direction
541    pub fn message_type(&self) -> &str {
542        match self {
543            ApplicationHeader::Input(header) => &header.message_type,
544            ApplicationHeader::Output(header) => &header.message_type,
545        }
546    }
547
548    /// Get the priority if available
549    pub fn priority(&self) -> Option<&str> {
550        match self {
551            ApplicationHeader::Input(header) => Some(&header.priority),
552            ApplicationHeader::Output(header) => header.priority.as_deref(),
553        }
554    }
555}
556
557impl std::fmt::Display for ApplicationHeader {
558    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559        match self {
560            ApplicationHeader::Input(header) => {
561                // Delegate to InputApplicationHeader's Display implementation
562                write!(f, "{}", header)
563            }
564            ApplicationHeader::Output(header) => {
565                // Output format: O + message_type + input_time + MIR + output_date + output_time + priority
566                // MIR = date + lt_address + session + sequence
567                let mut result = format!(
568                    "O{}{}{}{}{}{}{}{}",
569                    header.message_type,
570                    header.input_time,
571                    header.mir.date,
572                    header.mir.lt_identifier,
573                    header.mir.session_number,
574                    header.mir.sequence_number,
575                    header.output_date,
576                    header.output_time,
577                );
578
579                if let Some(ref priority) = header.priority {
580                    result.push_str(priority);
581                }
582
583                write!(f, "{result}")
584            }
585        }
586    }
587}
588
589impl std::fmt::Display for InputApplicationHeader {
590    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591        // Ensure consistent formatting:
592        // - message_type is always 3 digits
593        // - destination_address must be exactly 12 characters
594        // - priority is always 1 character
595
596        // Ensure message_type is exactly 3 characters
597        let message_type = format!(
598            "{:0>3}",
599            &self.message_type[..self.message_type.len().min(3)]
600        );
601
602        // Pad or truncate destination_address to exactly 12 characters
603        let destination_address = if self.destination_address.len() > 12 {
604            self.destination_address[..12].to_string()
605        } else if self.destination_address.len() < 12 {
606            format!("{:X<12}", self.destination_address)
607        } else {
608            self.destination_address.clone()
609        };
610
611        let mut result = format!("I{}{}{}", message_type, destination_address, self.priority);
612
613        if let Some(ref delivery_monitoring) = self.delivery_monitoring {
614            result.push_str(delivery_monitoring);
615        }
616
617        if let Some(ref obsolescence_period) = self.obsolescence_period {
618            result.push_str(obsolescence_period);
619        }
620
621        write!(f, "{result}")
622    }
623}
624
625impl std::fmt::Display for OutputApplicationHeader {
626    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627        let mut result = format!(
628            "O{}{}{}{}{}{}{}{}",
629            self.message_type,
630            self.input_time,
631            self.mir.date,
632            self.mir.lt_identifier,
633            self.mir.session_number,
634            self.mir.sequence_number,
635            self.output_date,
636            self.output_time,
637        );
638
639        if let Some(ref priority) = self.priority {
640            result.push_str(priority);
641        }
642
643        write!(f, "{result}")
644    }
645}
646
647/// **Block 3: User Header**
648///
649/// Optional service tags and controls for SWIFT messages.
650///
651/// **Format:** `{3:{tag:value}{tag:value}...}`
652/// **Common Tags:**
653/// - **121:** UETR (UUID format) - Mandatory for SWIFT gpi
654/// - **119:** Validation flag (STP, REMIT, COV, RFDD)
655/// - **103:** Service identifier
656/// - **108:** Message user reference
657/// - **433/434:** Sanctions screening and payment controls
658#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
659pub struct UserHeader {
660    /// Tag 103 - Service Identifier (3!a) - Mandatory for FINcopy Service
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub service_identifier: Option<String>,
663
664    /// Tag 113 - Banking Priority (4!x) - Optional
665    #[serde(skip_serializing_if = "Option::is_none")]
666    pub banking_priority: Option<String>,
667
668    /// Tag 108 - Message User Reference (16!x) - Optional
669    #[serde(skip_serializing_if = "Option::is_none")]
670    pub message_user_reference: Option<String>,
671
672    /// Tag 119 - Validation Flag (8c) - Optional (STP, REMIT, RFDD, COV)
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub validation_flag: Option<String>,
675
676    /// Tag 423 - Balance checkpoint date and time (YYMMDDHHMMSS\[ss\]) - Optional (MIRS only)
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub balance_checkpoint: Option<BalanceCheckpoint>,
679
680    /// Tag 106 - Message Input Reference MIR (28c) - Optional (MIRS only)
681    #[serde(skip_serializing_if = "Option::is_none")]
682    pub message_input_reference: Option<MessageInputReference>,
683
684    /// Tag 424 - Related reference (16x) - Optional (MIRS only)
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub related_reference: Option<String>,
687
688    /// Tag 111 - Service type identifier (3!n) - Optional
689    #[serde(skip_serializing_if = "Option::is_none")]
690    #[serde(rename = "service_type_identifier")]
691    pub service_type_identifier: Option<String>,
692
693    /// Tag 121 - Unique end-to-end transaction reference (UUID format) - Mandatory for GPI
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub unique_end_to_end_reference: Option<String>,
696
697    /// Tag 115 - Addressee Information (32x) - Optional (FINCopy only)
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub addressee_information: Option<String>,
700
701    /// Tag 165 - Payment release information receiver (3!c/34x) - Optional (FINInform only)
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub payment_release_information: Option<PaymentReleaseInfo>,
704
705    /// Tag 433 - Sanctions screening information (3!a/\[20x\]) - Optional
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
708
709    /// Tag 434 - Payment controls information (3!a/\[20x\]) - Optional
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub payment_controls_info: Option<PaymentControlsInfo>,
712}
713
714/// Balance checkpoint for Tag 423 (MIRS recovery)
715#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
716pub struct BalanceCheckpoint {
717    /// Date (YYMMDD)
718    pub date: String,
719    /// Time (HHMMSS)
720    pub time: String,
721    /// Hundredths of second (optional)
722    #[serde(skip_serializing_if = "Option::is_none")]
723    pub hundredths_of_second: Option<String>,
724}
725
726/// Message Input Reference (Tag 106, MIR format)
727#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
728pub struct MessageInputReference {
729    /// Date (YYMMDD)
730    pub date: String,
731    /// LT identifier (12 chars)
732    pub lt_identifier: String,
733    /// Branch code (3 chars)
734    pub branch_code: String,
735    /// Session number (4 digits)
736    pub session_number: String,
737    /// Sequence number (6 digits)
738    pub sequence_number: String,
739}
740
741/// Payment release info for Tag 165 (FINInform)
742#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
743pub struct PaymentReleaseInfo {
744    /// Code (3 chars)
745    pub code: String,
746    /// Additional info (max 34 chars)
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub additional_info: Option<String>,
749}
750
751/// Sanctions screening info for Tag 433
752#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
753pub struct SanctionsScreeningInfo {
754    /// Code word (AOK, FPO, NOK)
755    pub code_word: String,
756    /// Additional info (max 20 chars)
757    #[serde(skip_serializing_if = "Option::is_none")]
758    pub additional_info: Option<String>,
759}
760
761/// Payment controls info for Tag 434
762#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
763pub struct PaymentControlsInfo {
764    /// Code word (3 chars)
765    pub code_word: String,
766    /// Additional info (max 20 chars)
767    #[serde(skip_serializing_if = "Option::is_none")]
768    pub additional_info: Option<String>,
769}
770
771impl UserHeader {
772    /// Parse user header from block 3 string using structured parsing
773    pub fn parse(block3: &str) -> Result<Self> {
774        let mut user_header = UserHeader::default();
775
776        // Parse nested tags in format {tag:value}
777        // Simple parsing for now - more sophisticated regex parsing can be added later
778        if block3.contains("{103:")
779            && let Some(start) = block3.find("{103:")
780            && let Some(end) = block3[start..].find('}')
781        {
782            user_header.service_identifier = Some(block3[start + 5..start + end].to_string());
783        }
784
785        if block3.contains("{113:")
786            && let Some(start) = block3.find("{113:")
787            && let Some(end) = block3[start..].find('}')
788        {
789            user_header.banking_priority = Some(block3[start + 5..start + end].to_string());
790        }
791
792        if block3.contains("{108:")
793            && let Some(start) = block3.find("{108:")
794            && let Some(end) = block3[start..].find('}')
795        {
796            user_header.message_user_reference = Some(block3[start + 5..start + end].to_string());
797        }
798
799        if block3.contains("{119:")
800            && let Some(start) = block3.find("{119:")
801            && let Some(end) = block3[start..].find('}')
802        {
803            user_header.validation_flag = Some(block3[start + 5..start + end].to_string());
804        }
805
806        if block3.contains("{423:")
807            && let Some(start) = block3.find("{423:")
808            && let Some(end) = block3[start..].find('}')
809        {
810            let value = &block3[start + 5..start + end];
811            user_header.balance_checkpoint = Self::parse_balance_checkpoint(value);
812        }
813
814        if block3.contains("{106:")
815            && let Some(start) = block3.find("{106:")
816            && let Some(end) = block3[start..].find('}')
817        {
818            let value = &block3[start + 5..start + end];
819            user_header.message_input_reference = Self::parse_message_input_reference(value);
820        }
821
822        if block3.contains("{424:")
823            && let Some(start) = block3.find("{424:")
824            && let Some(end) = block3[start..].find('}')
825        {
826            user_header.related_reference = Some(block3[start + 5..start + end].to_string());
827        }
828
829        if block3.contains("{111:")
830            && let Some(start) = block3.find("{111:")
831            && let Some(end) = block3[start..].find('}')
832        {
833            user_header.service_type_identifier = Some(block3[start + 5..start + end].to_string());
834        }
835
836        if block3.contains("{121:")
837            && let Some(start) = block3.find("{121:")
838            && let Some(end) = block3[start..].find('}')
839        {
840            user_header.unique_end_to_end_reference =
841                Some(block3[start + 5..start + end].to_string());
842        }
843
844        if block3.contains("{115:")
845            && let Some(start) = block3.find("{115:")
846            && let Some(end) = block3[start..].find('}')
847        {
848            user_header.addressee_information = Some(block3[start + 5..start + end].to_string());
849        }
850
851        if block3.contains("{165:")
852            && let Some(start) = block3.find("{165:")
853            && let Some(end) = block3[start..].find('}')
854        {
855            let value = &block3[start + 5..start + end];
856            user_header.payment_release_information = Self::parse_payment_release_info(value);
857        }
858
859        if block3.contains("{433:")
860            && let Some(start) = block3.find("{433:")
861            && let Some(end) = block3[start..].find('}')
862        {
863            let value = &block3[start + 5..start + end];
864            user_header.sanctions_screening_info = Self::parse_sanctions_screening_info(value);
865        }
866
867        if block3.contains("{434:")
868            && let Some(start) = block3.find("{434:")
869            && let Some(end) = block3[start..].find('}')
870        {
871            let value = &block3[start + 5..start + end];
872            user_header.payment_controls_info = Self::parse_payment_controls_info(value);
873        }
874
875        Ok(user_header)
876    }
877
878    /// Parse balance checkpoint from tag value
879    fn parse_balance_checkpoint(value: &str) -> Option<BalanceCheckpoint> {
880        if value.len() >= 12 {
881            Some(BalanceCheckpoint {
882                date: value[0..6].to_string(),
883                time: value[6..12].to_string(),
884                hundredths_of_second: if value.len() > 12 {
885                    Some(value[12..].to_string())
886                } else {
887                    None
888                },
889            })
890        } else {
891            None
892        }
893    }
894
895    /// Parse message input reference from tag value
896    fn parse_message_input_reference(value: &str) -> Option<MessageInputReference> {
897        if value.len() >= 28 {
898            Some(MessageInputReference {
899                date: value[0..6].to_string(),
900                lt_identifier: value[6..18].to_string(),
901                branch_code: value[18..21].to_string(),
902                session_number: value[21..25].to_string(),
903                sequence_number: value[25..].to_string(),
904            })
905        } else {
906            None
907        }
908    }
909
910    /// Parse payment release info from tag value
911    fn parse_payment_release_info(value: &str) -> Option<PaymentReleaseInfo> {
912        if value.len() >= 3 {
913            let code = value[0..3].to_string();
914            let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
915                Some(value[4..].to_string())
916            } else {
917                None
918            };
919            Some(PaymentReleaseInfo {
920                code,
921                additional_info,
922            })
923        } else {
924            None
925        }
926    }
927
928    /// Parse sanctions screening info from tag value
929    fn parse_sanctions_screening_info(value: &str) -> Option<SanctionsScreeningInfo> {
930        if value.len() >= 3 {
931            let code_word = value[0..3].to_string();
932            let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
933                Some(value[4..].to_string())
934            } else {
935                None
936            };
937            Some(SanctionsScreeningInfo {
938                code_word,
939                additional_info,
940            })
941        } else {
942            None
943        }
944    }
945
946    /// Parse payment controls info from tag value
947    fn parse_payment_controls_info(value: &str) -> Option<PaymentControlsInfo> {
948        if value.len() >= 3 {
949            let code_word = value[0..3].to_string();
950            let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
951                Some(value[4..].to_string())
952            } else {
953                None
954            };
955            Some(PaymentControlsInfo {
956                code_word,
957                additional_info,
958            })
959        } else {
960            None
961        }
962    }
963}
964
965impl std::fmt::Display for UserHeader {
966    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
967        let mut result = String::new();
968
969        if let Some(ref service_id) = self.service_identifier {
970            result.push_str(&format!("{{103:{service_id}}}"));
971        }
972
973        if let Some(ref banking_priority) = self.banking_priority {
974            result.push_str(&format!("{{113:{banking_priority}}}"));
975        }
976
977        if let Some(ref message_user_ref) = self.message_user_reference {
978            result.push_str(&format!("{{108:{message_user_ref}}}"));
979        }
980
981        if let Some(ref validation_flag) = self.validation_flag {
982            result.push_str(&format!("{{119:{validation_flag}}}"));
983        }
984
985        if let Some(ref unique_end_to_end_ref) = self.unique_end_to_end_reference {
986            result.push_str(&format!("{{121:{unique_end_to_end_ref}}}"));
987        }
988
989        if let Some(ref service_type_identifier) = self.service_type_identifier {
990            result.push_str(&format!("{{111:{service_type_identifier}}}"));
991        }
992
993        if let Some(ref payment_controls) = self.payment_controls_info {
994            let mut value = payment_controls.code_word.clone();
995            if let Some(ref additional) = payment_controls.additional_info {
996                value.push('/');
997                value.push_str(additional);
998            }
999            result.push_str(&format!("{{434:{value}}}"));
1000        }
1001
1002        if let Some(ref payment_release) = self.payment_release_information {
1003            let mut value = payment_release.code.clone();
1004            if let Some(ref additional) = payment_release.additional_info {
1005                value.push('/');
1006                value.push_str(additional);
1007            }
1008            result.push_str(&format!("{{165:{value}}}"));
1009        }
1010
1011        if let Some(ref sanctions) = self.sanctions_screening_info {
1012            let mut value = sanctions.code_word.clone();
1013            if let Some(ref additional) = sanctions.additional_info {
1014                value.push('/');
1015                value.push_str(additional);
1016            }
1017            result.push_str(&format!("{{433:{value}}}"));
1018        }
1019
1020        write!(f, "{result}")
1021    }
1022}
1023
1024/// **Block 5: Trailer**
1025///
1026/// Security and control information for message integrity and authentication.
1027///
1028/// **Format:** `{5:{tag:value}{tag:value}...}`
1029/// **Key Tags:**
1030/// - **CHK:** Checksum (12 hex chars) - Mandatory for integrity validation
1031/// - **MAC:** Message Authentication Code - Optional bilateral authentication
1032/// - **TNG:** Test and Training flag - Identifies test messages
1033/// - **PDM/PDE:** Possible duplicate detection tags
1034/// - **DLM:** Delayed message indicator
1035#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1036pub struct Trailer {
1037    /// CHK - Checksum (12!h) - Mandatory
1038    #[serde(skip_serializing_if = "Option::is_none")]
1039    pub checksum: Option<String>,
1040
1041    /// TNG - Test & Training Message - Optional (empty tag)
1042    #[serde(skip_serializing_if = "Option::is_none")]
1043    pub test_and_training: Option<bool>,
1044
1045    /// PDE - Possible Duplicate Emission - Optional
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
1048
1049    /// DLM - Delayed Message - Optional (empty tag)
1050    #[serde(skip_serializing_if = "Option::is_none")]
1051    pub delayed_message: Option<bool>,
1052
1053    /// MRF - Message Reference - Optional
1054    #[serde(skip_serializing_if = "Option::is_none")]
1055    pub message_reference: Option<MessageReference>,
1056
1057    /// PDM - Possible Duplicate Message - Optional
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
1060
1061    /// SYS - System Originated Message - Optional
1062    #[serde(skip_serializing_if = "Option::is_none")]
1063    pub system_originated_message: Option<SystemOriginatedMessage>,
1064
1065    /// MAC - Message Authentication Code - Optional
1066    #[serde(skip_serializing_if = "Option::is_none")]
1067    pub mac: Option<String>,
1068}
1069
1070/// Possible Duplicate Emission for PDE tag
1071#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1072pub struct PossibleDuplicateEmission {
1073    /// Time (HHMM)
1074    #[serde(skip_serializing_if = "Option::is_none")]
1075    pub time: Option<String>,
1076    /// Message Input Reference
1077    #[serde(skip_serializing_if = "Option::is_none")]
1078    pub message_input_reference: Option<MessageInputReference>,
1079}
1080
1081/// Message Reference for MRF tag
1082#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1083pub struct MessageReference {
1084    /// Date (YYMMDD)
1085    pub date: String,
1086    /// Time (HHMM)
1087    pub full_time: String,
1088    /// Message Input Reference
1089    pub message_input_reference: MessageInputReference,
1090}
1091
1092/// Possible Duplicate Message for PDM tag
1093#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1094pub struct PossibleDuplicateMessage {
1095    /// Time (HHMM)
1096    #[serde(skip_serializing_if = "Option::is_none")]
1097    pub time: Option<String>,
1098    /// Message Output Reference
1099    #[serde(skip_serializing_if = "Option::is_none")]
1100    pub message_output_reference: Option<MessageOutputReference>,
1101}
1102
1103/// Message Output Reference (MOR format)
1104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1105pub struct MessageOutputReference {
1106    /// Date (YYMMDD)
1107    pub date: String,
1108    /// LT identifier (12 chars)
1109    pub lt_identifier: String,
1110    /// Branch code (3 chars)
1111    pub branch_code: String,
1112    /// Session number (4 digits)
1113    pub session_number: String,
1114    /// Sequence number (6 digits)
1115    pub sequence_number: String,
1116}
1117
1118/// System Originated Message for SYS tag
1119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1120pub struct SystemOriginatedMessage {
1121    /// Time (HHMM)
1122    #[serde(skip_serializing_if = "Option::is_none")]
1123    pub time: Option<String>,
1124    /// Message Input Reference
1125    #[serde(skip_serializing_if = "Option::is_none")]
1126    pub message_input_reference: Option<MessageInputReference>,
1127}
1128
1129impl Trailer {
1130    /// Parse trailer from block 5 string using structured parsing
1131    pub fn parse(block5: &str) -> Result<Self> {
1132        let mut trailer = Trailer::default();
1133
1134        // Extract common tags if present
1135        if block5.contains("{CHK:")
1136            && let Some(start) = block5.find("{CHK:")
1137            && let Some(end) = block5[start..].find('}')
1138        {
1139            trailer.checksum = Some(block5[start + 5..start + end].to_string());
1140        }
1141
1142        if block5.contains("{TNG}") {
1143            trailer.test_and_training = Some(true);
1144        }
1145
1146        if block5.contains("{DLM}") {
1147            trailer.delayed_message = Some(true);
1148        }
1149
1150        if block5.contains("{MAC:")
1151            && let Some(start) = block5.find("{MAC:")
1152            && let Some(end) = block5[start..].find('}')
1153        {
1154            trailer.mac = Some(block5[start + 5..start + end].to_string());
1155        }
1156
1157        // More complex parsing for structured tags can be added here
1158        // For now, implementing basic tag extraction
1159
1160        Ok(trailer)
1161    }
1162}
1163
1164impl std::fmt::Display for Trailer {
1165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1166        let mut result = String::new();
1167
1168        if let Some(ref checksum) = self.checksum {
1169            result.push_str(&format!("{{CHK:{checksum}}}"));
1170        }
1171
1172        if let Some(true) = self.test_and_training {
1173            result.push_str("{TNG}");
1174        }
1175
1176        if let Some(true) = self.delayed_message {
1177            result.push_str("{DLM}");
1178        }
1179
1180        if let Some(ref possible_duplicate_emission) = self.possible_duplicate_emission {
1181            result.push_str(&format!(
1182                "{{PDE:{}}}",
1183                possible_duplicate_emission.time.as_deref().unwrap_or("")
1184            ));
1185        }
1186
1187        if let Some(ref message_reference) = self.message_reference {
1188            result.push_str(&format!("{{MRF:{}}}", message_reference.date));
1189        }
1190
1191        if let Some(ref mac) = self.mac {
1192            result.push_str(&format!("{{MAC:{mac}}}"));
1193        }
1194
1195        write!(f, "{result}")
1196    }
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201    use super::*;
1202
1203    #[test]
1204    fn test_application_header_input_parsing() {
1205        // Test Input message format parsing
1206        let block2 = "I103DEUTDEFFAXXXN";
1207        let header = ApplicationHeader::parse(block2).unwrap();
1208
1209        match header {
1210            ApplicationHeader::Input(input) => {
1211                assert_eq!(input.message_type, "103");
1212                assert_eq!(input.destination_address, "DEUTDEFFAXXX");
1213                assert_eq!(input.receiver_bic, "DEUTDEFF");
1214                assert_eq!(input.priority, "N");
1215                assert_eq!(input.delivery_monitoring, None);
1216                assert_eq!(input.obsolescence_period, None);
1217            }
1218            ApplicationHeader::Output(_) => panic!("Expected Input header, got Output"),
1219        }
1220    }
1221
1222    #[test]
1223    fn test_application_header_input_parsing_with_monitoring() {
1224        // Test Input message with delivery monitoring
1225        let block2 = "I103DEUTDEFFAXXXU3003";
1226        let header = ApplicationHeader::parse(block2).unwrap();
1227
1228        match header {
1229            ApplicationHeader::Input(input) => {
1230                assert_eq!(input.message_type, "103");
1231                assert_eq!(input.destination_address, "DEUTDEFFAXXX");
1232                assert_eq!(input.receiver_bic, "DEUTDEFF");
1233                assert_eq!(input.priority, "U");
1234                assert_eq!(input.delivery_monitoring, Some("3".to_string()));
1235                assert_eq!(input.obsolescence_period, Some("003".to_string()));
1236            }
1237            ApplicationHeader::Output(_) => panic!("Expected Input header, got Output"),
1238        }
1239    }
1240
1241    #[test]
1242    fn test_application_header_output_parsing() {
1243        // Test Output message format parsing - the exact case from the issue
1244        let block2 = "O1031535051028DEUTDEFFAXXX08264556280510281535N";
1245        let header = ApplicationHeader::parse(block2).unwrap();
1246
1247        match header {
1248            ApplicationHeader::Output(output) => {
1249                assert_eq!(output.message_type, "103");
1250                assert_eq!(output.input_time, "1535");
1251                assert_eq!(output.output_date, "051028");
1252                assert_eq!(output.output_time, "1535");
1253                assert_eq!(output.priority, Some("N".to_string()));
1254
1255                // Check MIR structure
1256                assert_eq!(output.mir.date, "051028");
1257                assert_eq!(output.mir.lt_identifier, "DEUTDEFFAXXX");
1258                assert_eq!(output.mir.branch_code, "XXX");
1259                assert_eq!(output.mir.session_number, "0826");
1260                assert_eq!(output.mir.sequence_number, "455628");
1261            }
1262            ApplicationHeader::Input(_) => panic!("Expected Output header, got Input"),
1263        }
1264    }
1265
1266    #[test]
1267    fn test_application_header_output_parsing_different_message_type() {
1268        // Test another Output message format
1269        let block2 = "O2021245051028CHASUS33AXXX08264556280510281245U";
1270        let header = ApplicationHeader::parse(block2).unwrap();
1271
1272        match header {
1273            ApplicationHeader::Output(output) => {
1274                assert_eq!(output.message_type, "202");
1275                assert_eq!(output.mir.lt_identifier, "CHASUS33AXXX");
1276                assert_eq!(output.priority, Some("U".to_string()));
1277            }
1278            ApplicationHeader::Input(_) => panic!("Expected Output header, got Input"),
1279        }
1280    }
1281
1282    #[test]
1283    fn test_application_header_invalid_direction() {
1284        let block2 = "X103DEUTDEFFAXXXN";
1285        let result = ApplicationHeader::parse(block2);
1286
1287        assert!(result.is_err());
1288        if let Err(ParseError::InvalidBlockStructure { message, .. }) = result {
1289            assert!(message.contains("Invalid direction indicator"));
1290        } else {
1291            panic!("Expected InvalidBlockStructure error");
1292        }
1293    }
1294
1295    #[test]
1296    fn test_application_header_input_too_short() {
1297        let block2 = "I103DEUTDEF"; // Too short for Input format
1298        let result = ApplicationHeader::parse(block2);
1299
1300        assert!(result.is_err());
1301    }
1302
1303    #[test]
1304    fn test_application_header_output_too_short() {
1305        let block2 = "O103153505102"; // Too short for Output format (13 characters)
1306        let result = ApplicationHeader::parse(block2);
1307
1308        assert!(result.is_err());
1309        if let Err(ParseError::InvalidBlockStructure { message, .. }) = result {
1310            // This will now hit the Output-specific check since initial check is for 4 chars
1311            assert!(message.contains("Output Block 2 too short: expected at least 46 characters"));
1312        } else {
1313            panic!("Expected InvalidBlockStructure error");
1314        }
1315    }
1316
1317    #[test]
1318    fn test_application_header_output_minimum_length_but_still_too_short() {
1319        // This has 17 characters so it passes initial check but fails Output-specific check
1320        let block2 = "O10315350510280DE"; // 17 characters, but Output needs 46
1321        let result = ApplicationHeader::parse(block2);
1322
1323        assert!(result.is_err());
1324        if let Err(ParseError::InvalidBlockStructure { message, .. }) = result {
1325            assert!(message.contains("Output Block 2 too short: expected at least 46 characters"));
1326        } else {
1327            panic!("Expected InvalidBlockStructure error");
1328        }
1329    }
1330
1331    #[test]
1332    fn test_basic_header_parsing() {
1333        let block1 = "F01DEUTDEFFAXXX0000123456";
1334        let header = BasicHeader::parse(block1).unwrap();
1335
1336        assert_eq!(header.application_id, "F");
1337        assert_eq!(header.service_id, "01");
1338        assert_eq!(header.logical_terminal, "DEUTDEFFAXXX");
1339        assert_eq!(header.sender_bic, "DEUTDEFF");
1340        assert_eq!(header.session_number, "0000");
1341        assert_eq!(header.sequence_number, "123456");
1342    }
1343
1344    #[test]
1345    fn test_application_header_input_display() {
1346        let header = ApplicationHeader::Input(InputApplicationHeader {
1347            message_type: "103".to_string(),
1348            destination_address: "DEUTDEFFAXXX".to_string(),
1349            receiver_bic: "DEUTDEFF".to_string(),
1350            priority: "U".to_string(),
1351            delivery_monitoring: Some("3".to_string()),
1352            obsolescence_period: Some("003".to_string()),
1353        });
1354
1355        assert_eq!(header.to_string(), "I103DEUTDEFFAXXXU3003");
1356    }
1357
1358    #[test]
1359    fn test_application_header_output_display() {
1360        let mir = MessageInputReference {
1361            date: "051028".to_string(),
1362            lt_identifier: "DEUTDEFFAXXX".to_string(),
1363            branch_code: "XXX".to_string(),
1364            session_number: "0826".to_string(),
1365            sequence_number: "455628".to_string(),
1366        };
1367
1368        let header = ApplicationHeader::Output(OutputApplicationHeader {
1369            message_type: "103".to_string(),
1370            input_time: "1535".to_string(),
1371            mir,
1372            output_date: "051028".to_string(),
1373            output_time: "1535".to_string(),
1374            priority: Some("N".to_string()),
1375        });
1376
1377        assert_eq!(
1378            header.to_string(),
1379            "O1031535051028DEUTDEFFAXXX08264556280510281535N"
1380        );
1381    }
1382
1383    #[test]
1384    fn test_application_header_helper_methods() {
1385        let input_header = ApplicationHeader::Input(InputApplicationHeader {
1386            message_type: "103".to_string(),
1387            destination_address: "DEUTDEFFAXXX".to_string(),
1388            receiver_bic: "DEUTDEFF".to_string(),
1389            priority: "U".to_string(),
1390            delivery_monitoring: None,
1391            obsolescence_period: None,
1392        });
1393
1394        assert_eq!(input_header.message_type(), "103");
1395        assert_eq!(input_header.priority(), Some("U"));
1396
1397        let mir = MessageInputReference {
1398            date: "051028".to_string(),
1399            lt_identifier: "DEUTDEFFAXXX".to_string(),
1400            branch_code: "XXX".to_string(),
1401            session_number: "0826".to_string(),
1402            sequence_number: "455628".to_string(),
1403        };
1404
1405        let output_header = ApplicationHeader::Output(OutputApplicationHeader {
1406            message_type: "202".to_string(),
1407            input_time: "1535".to_string(),
1408            mir,
1409            output_date: "051028".to_string(),
1410            output_time: "1535".to_string(),
1411            priority: Some("N".to_string()),
1412        });
1413
1414        assert_eq!(output_header.message_type(), "202");
1415        assert_eq!(output_header.priority(), Some("N"));
1416    }
1417}