Skip to main content

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