swift_mt_message/headers/
mod.rs

1use crate::errors::{ParseError, Result};
2use crate::fields::common::BIC;
3use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5
6/// Basic Header (Block 1) - Application and service identifier
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct BasicHeader {
9    /// Application identifier (F = FIN application)
10    pub application_id: String,
11
12    /// Service identifier (01 = FIN)
13    pub service_id: String,
14
15    /// Logical Terminal (LT) address (12 characters)
16    pub logical_terminal: String,
17    pub sender_bic: BIC,
18
19    /// Session number (4 digits)
20    pub session_number: String,
21
22    /// Sequence number (6 digits)
23    pub sequence_number: String,
24}
25
26impl BasicHeader {
27    /// Parse basic header from block 1 string
28    pub fn parse(block1: &str) -> Result<Self> {
29        if block1.len() < 21 {
30            return Err(ParseError::InvalidBlockStructure {
31                message: format!(
32                    "Block 1 too short: expected at least 21 characters, got {}",
33                    block1.len()
34                ),
35            });
36        }
37
38        let application_id = block1[0..1].to_string();
39        let service_id = block1[1..3].to_string();
40        let logical_terminal = block1[3..15].to_string();
41        let session_number = block1[15..19].to_string();
42        let sequence_number = block1[19..].to_string();
43
44        // Extract BIC from logical terminal (first 8 characters for standard BIC)
45        let bic_str = &logical_terminal[0..8];
46        let sender_bic = BIC::from_str(bic_str).map_err(|e| ParseError::InvalidBlockStructure {
47            message: format!("Failed to parse BIC from logical terminal '{bic_str}': {e}"),
48        })?;
49
50        Ok(BasicHeader {
51            application_id,
52            service_id,
53            logical_terminal,
54            sender_bic,
55            session_number,
56            sequence_number,
57        })
58    }
59}
60
61impl std::fmt::Display for BasicHeader {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(
64            f,
65            "{}{}{}{}{}",
66            self.application_id,
67            self.service_id,
68            self.logical_terminal,
69            self.session_number,
70            self.sequence_number
71        )
72    }
73}
74
75/// Application Header (Block 2) - Message information
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct ApplicationHeader {
78    /// Direction (I = Input, O = Output)
79    pub direction: String,
80
81    /// Message type (e.g., "103", "202")
82    pub message_type: String,
83
84    /// Destination address (12 characters)
85    pub destination_address: String,
86    pub receiver_bic: BIC,
87
88    /// Priority (U = Urgent, N = Normal, S = System)
89    pub priority: String,
90
91    /// Delivery monitoring (1 = Non-delivery notification, 3 = Delivery notification)
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub delivery_monitoring: Option<String>,
94
95    /// Obsolescence period (3 digits, only for certain message types)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub obsolescence_period: Option<String>,
98}
99
100impl ApplicationHeader {
101    /// Parse application header from block 2 string
102    pub fn parse(block2: &str) -> Result<Self> {
103        if block2.len() < 17 {
104            return Err(ParseError::InvalidBlockStructure {
105                message: format!(
106                    "Block 2 too short: expected at least 18 characters, got {}",
107                    block2.len()
108                ),
109            });
110        }
111
112        let direction = block2[0..1].to_string();
113        let message_type = block2[1..4].to_string();
114        let destination_address = block2[4..16].to_string();
115        let priority = block2[16..17].to_string();
116
117        let delivery_monitoring = if block2.len() >= 18 {
118            Some(block2[17..18].to_string())
119        } else {
120            None
121        };
122
123        let obsolescence_period = if block2.len() >= 21 {
124            Some(block2[18..21].to_string())
125        } else {
126            None
127        };
128
129        let receiver_bic = BIC::from_str(&destination_address[0..8]).map_err(|e| {
130            ParseError::InvalidBlockStructure {
131                message: format!(
132                    "Failed to parse BIC from destination address '{destination_address}': {e}"
133                ),
134            }
135        })?;
136
137        Ok(ApplicationHeader {
138            direction,
139            message_type,
140            destination_address,
141            receiver_bic,
142            priority,
143            delivery_monitoring,
144            obsolescence_period,
145        })
146    }
147}
148
149impl std::fmt::Display for ApplicationHeader {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        let mut result = format!(
152            "{}{}{}{}",
153            self.direction, self.message_type, self.destination_address, self.priority
154        );
155
156        if let Some(ref delivery_monitoring) = self.delivery_monitoring {
157            result.push_str(delivery_monitoring);
158        }
159
160        if let Some(ref obsolescence_period) = self.obsolescence_period {
161            result.push_str(obsolescence_period);
162        }
163
164        write!(f, "{result}")
165    }
166}
167
168/// User Header (Block 3) structure based on SWIFT MT standards
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
170pub struct UserHeader {
171    /// Tag 103 - Service Identifier (3!a) - Mandatory for FINcopy Service
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub service_identifier: Option<String>,
174
175    /// Tag 113 - Banking Priority (4!x) - Optional
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub banking_priority: Option<String>,
178
179    /// Tag 108 - Message User Reference (16!x) - Optional
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub message_user_reference: Option<String>,
182
183    /// Tag 119 - Validation Flag (8c) - Optional (STP, REMIT, RFDD, COV)
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub validation_flag: Option<String>,
186
187    /// Tag 423 - Balance checkpoint date and time (YYMMDDHHMMSS[ss]) - Optional (MIRS only)
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub balance_checkpoint: Option<BalanceCheckpoint>,
190
191    /// Tag 106 - Message Input Reference MIR (28c) - Optional (MIRS only)
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub message_input_reference: Option<MessageInputReference>,
194
195    /// Tag 424 - Related reference (16x) - Optional (MIRS only)
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub related_reference: Option<String>,
198
199    /// Tag 111 - Service type identifier (3!n) - Optional
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub service_type_identifier: Option<String>,
202
203    /// Tag 121 - Unique end-to-end transaction reference (UUID format) - Mandatory for GPI
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub unique_end_to_end_reference: Option<String>,
206
207    /// Tag 115 - Addressee Information (32x) - Optional (FINCopy only)
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub addressee_information: Option<String>,
210
211    /// Tag 165 - Payment release information receiver (3!c/34x) - Optional (FINInform only)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub payment_release_information: Option<PaymentReleaseInfo>,
214
215    /// Tag 433 - Sanctions screening information (3!a/[20x]) - Optional
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
218
219    /// Tag 434 - Payment controls information (3!a/[20x]) - Optional
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub payment_controls_info: Option<PaymentControlsInfo>,
222}
223
224/// Balance checkpoint structure for Tag 423
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
226pub struct BalanceCheckpoint {
227    pub date: String, // YYMMDD
228    pub time: String, // HHMMSS
229
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub hundredths_of_second: Option<String>, // ss (optional)
232}
233
234/// Message Input Reference structure for Tag 106
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
236pub struct MessageInputReference {
237    pub date: String,            // YYMMDD
238    pub lt_identifier: String,   // 12 characters
239    pub branch_code: String,     // 3!c
240    pub session_number: String,  // 4!n
241    pub sequence_number: String, // 6!n
242}
243
244/// Payment release information structure for Tag 165
245#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
246pub struct PaymentReleaseInfo {
247    pub code: String, // 3!c
248
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub additional_info: Option<String>, // 34x (optional)
251}
252
253/// Sanctions screening information structure for Tag 433
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
255pub struct SanctionsScreeningInfo {
256    pub code_word: String, // 3!a (AOK, FPO, NOK)
257
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub additional_info: Option<String>, // 20x (optional)
260}
261
262/// Payment controls information structure for Tag 434
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264pub struct PaymentControlsInfo {
265    pub code_word: String, // 3!a
266
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub additional_info: Option<String>, // 20x (optional)
269}
270
271impl UserHeader {
272    /// Parse user header from block 3 string using structured parsing
273    pub fn parse(block3: &str) -> Result<Self> {
274        let mut user_header = UserHeader::default();
275
276        // Parse nested tags in format {tag:value}
277        // Simple parsing for now - more sophisticated regex parsing can be added later
278        if block3.contains("{103:") {
279            if let Some(start) = block3.find("{103:") {
280                if let Some(end) = block3[start..].find('}') {
281                    user_header.service_identifier =
282                        Some(block3[start + 5..start + end].to_string());
283                }
284            }
285        }
286
287        if block3.contains("{113:") {
288            if let Some(start) = block3.find("{113:") {
289                if let Some(end) = block3[start..].find('}') {
290                    user_header.banking_priority = Some(block3[start + 5..start + end].to_string());
291                }
292            }
293        }
294
295        if block3.contains("{108:") {
296            if let Some(start) = block3.find("{108:") {
297                if let Some(end) = block3[start..].find('}') {
298                    user_header.message_user_reference =
299                        Some(block3[start + 5..start + end].to_string());
300                }
301            }
302        }
303
304        if block3.contains("{119:") {
305            if let Some(start) = block3.find("{119:") {
306                if let Some(end) = block3[start..].find('}') {
307                    user_header.validation_flag = Some(block3[start + 5..start + end].to_string());
308                }
309            }
310        }
311
312        if block3.contains("{423:") {
313            if let Some(start) = block3.find("{423:") {
314                if let Some(end) = block3[start..].find('}') {
315                    let value = &block3[start + 5..start + end];
316                    user_header.balance_checkpoint = Self::parse_balance_checkpoint(value);
317                }
318            }
319        }
320
321        if block3.contains("{106:") {
322            if let Some(start) = block3.find("{106:") {
323                if let Some(end) = block3[start..].find('}') {
324                    let value = &block3[start + 5..start + end];
325                    user_header.message_input_reference =
326                        Self::parse_message_input_reference(value);
327                }
328            }
329        }
330
331        if block3.contains("{424:") {
332            if let Some(start) = block3.find("{424:") {
333                if let Some(end) = block3[start..].find('}') {
334                    user_header.related_reference =
335                        Some(block3[start + 5..start + end].to_string());
336                }
337            }
338        }
339
340        if block3.contains("{111:") {
341            if let Some(start) = block3.find("{111:") {
342                if let Some(end) = block3[start..].find('}') {
343                    user_header.service_type_identifier =
344                        Some(block3[start + 5..start + end].to_string());
345                }
346            }
347        }
348
349        if block3.contains("{121:") {
350            if let Some(start) = block3.find("{121:") {
351                if let Some(end) = block3[start..].find('}') {
352                    user_header.unique_end_to_end_reference =
353                        Some(block3[start + 5..start + end].to_string());
354                }
355            }
356        }
357
358        if block3.contains("{115:") {
359            if let Some(start) = block3.find("{115:") {
360                if let Some(end) = block3[start..].find('}') {
361                    user_header.addressee_information =
362                        Some(block3[start + 5..start + end].to_string());
363                }
364            }
365        }
366
367        if block3.contains("{165:") {
368            if let Some(start) = block3.find("{165:") {
369                if let Some(end) = block3[start..].find('}') {
370                    let value = &block3[start + 5..start + end];
371                    user_header.payment_release_information =
372                        Self::parse_payment_release_info(value);
373                }
374            }
375        }
376
377        if block3.contains("{433:") {
378            if let Some(start) = block3.find("{433:") {
379                if let Some(end) = block3[start..].find('}') {
380                    let value = &block3[start + 5..start + end];
381                    user_header.sanctions_screening_info =
382                        Self::parse_sanctions_screening_info(value);
383                }
384            }
385        }
386
387        if block3.contains("{434:") {
388            if let Some(start) = block3.find("{434:") {
389                if let Some(end) = block3[start..].find('}') {
390                    let value = &block3[start + 5..start + end];
391                    user_header.payment_controls_info = Self::parse_payment_controls_info(value);
392                }
393            }
394        }
395
396        Ok(user_header)
397    }
398
399    /// Parse balance checkpoint from tag value
400    fn parse_balance_checkpoint(value: &str) -> Option<BalanceCheckpoint> {
401        if value.len() >= 12 {
402            Some(BalanceCheckpoint {
403                date: value[0..6].to_string(),
404                time: value[6..12].to_string(),
405                hundredths_of_second: if value.len() > 12 {
406                    Some(value[12..].to_string())
407                } else {
408                    None
409                },
410            })
411        } else {
412            None
413        }
414    }
415
416    /// Parse message input reference from tag value
417    fn parse_message_input_reference(value: &str) -> Option<MessageInputReference> {
418        if value.len() >= 28 {
419            Some(MessageInputReference {
420                date: value[0..6].to_string(),
421                lt_identifier: value[6..18].to_string(),
422                branch_code: value[18..21].to_string(),
423                session_number: value[21..25].to_string(),
424                sequence_number: value[25..].to_string(),
425            })
426        } else {
427            None
428        }
429    }
430
431    /// Parse payment release info from tag value
432    fn parse_payment_release_info(value: &str) -> Option<PaymentReleaseInfo> {
433        if value.len() >= 3 {
434            let code = value[0..3].to_string();
435            let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
436                Some(value[4..].to_string())
437            } else {
438                None
439            };
440            Some(PaymentReleaseInfo {
441                code,
442                additional_info,
443            })
444        } else {
445            None
446        }
447    }
448
449    /// Parse sanctions screening info from tag value
450    fn parse_sanctions_screening_info(value: &str) -> Option<SanctionsScreeningInfo> {
451        if value.len() >= 3 {
452            let code_word = value[0..3].to_string();
453            let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
454                Some(value[4..].to_string())
455            } else {
456                None
457            };
458            Some(SanctionsScreeningInfo {
459                code_word,
460                additional_info,
461            })
462        } else {
463            None
464        }
465    }
466
467    /// Parse payment controls info from tag value
468    fn parse_payment_controls_info(value: &str) -> Option<PaymentControlsInfo> {
469        if value.len() >= 3 {
470            let code_word = value[0..3].to_string();
471            let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
472                Some(value[4..].to_string())
473            } else {
474                None
475            };
476            Some(PaymentControlsInfo {
477                code_word,
478                additional_info,
479            })
480        } else {
481            None
482        }
483    }
484}
485
486impl std::fmt::Display for UserHeader {
487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        let mut result = String::new();
489
490        if let Some(ref service_id) = self.service_identifier {
491            result.push_str(&format!("{{103:{service_id}}}"));
492        }
493
494        if let Some(ref banking_priority) = self.banking_priority {
495            result.push_str(&format!("{{113:{banking_priority}}}"));
496        }
497
498        if let Some(ref message_user_ref) = self.message_user_reference {
499            result.push_str(&format!("{{108:{message_user_ref}}}"));
500        }
501
502        if let Some(ref validation_flag) = self.validation_flag {
503            result.push_str(&format!("{{119:{validation_flag}}}"));
504        }
505
506        if let Some(ref unique_end_to_end_ref) = self.unique_end_to_end_reference {
507            result.push_str(&format!("{{121:{unique_end_to_end_ref}}}"));
508        }
509
510        if let Some(ref service_type_id) = self.service_type_identifier {
511            result.push_str(&format!("{{111:{service_type_id}}}"));
512        }
513
514        if let Some(ref payment_controls) = self.payment_controls_info {
515            let mut value = payment_controls.code_word.clone();
516            if let Some(ref additional) = payment_controls.additional_info {
517                value.push('/');
518                value.push_str(additional);
519            }
520            result.push_str(&format!("{{434:{value}}}"));
521        }
522
523        if let Some(ref payment_release) = self.payment_release_information {
524            let mut value = payment_release.code.clone();
525            if let Some(ref additional) = payment_release.additional_info {
526                value.push('/');
527                value.push_str(additional);
528            }
529            result.push_str(&format!("{{165:{value}}}"));
530        }
531
532        if let Some(ref sanctions) = self.sanctions_screening_info {
533            let mut value = sanctions.code_word.clone();
534            if let Some(ref additional) = sanctions.additional_info {
535                value.push('/');
536                value.push_str(additional);
537            }
538            result.push_str(&format!("{{433:{value}}}"));
539        }
540
541        write!(f, "{result}")
542    }
543}
544
545/// Trailer (Block 5) structure based on SWIFT MT standards
546#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
547pub struct Trailer {
548    /// CHK - Checksum (12!h) - Mandatory
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub checksum: Option<String>,
551
552    /// TNG - Test & Training Message - Optional (empty tag)
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub test_and_training: Option<bool>,
555
556    /// PDE - Possible Duplicate Emission - Optional
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
559
560    /// DLM - Delayed Message - Optional (empty tag)
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub delayed_message: Option<bool>,
563
564    /// MRF - Message Reference - Optional
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub message_reference: Option<MessageReference>,
567
568    /// PDM - Possible Duplicate Message - Optional
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
571
572    /// SYS - System Originated Message - Optional
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub system_originated_message: Option<SystemOriginatedMessage>,
575
576    /// MAC - Message Authentication Code - Optional
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub mac: Option<String>,
579}
580
581/// Possible Duplicate Emission structure for PDE tag
582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
583pub struct PossibleDuplicateEmission {
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub time: Option<String>, // HHMM (optional)
586
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub message_input_reference: Option<MessageInputReference>, // MIR (optional)
589}
590
591/// Message Reference structure for MRF tag
592#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
593pub struct MessageReference {
594    pub date: String,                                   // YYMMDD
595    pub full_time: String,                              // HHMM
596    pub message_input_reference: MessageInputReference, // MIR
597}
598
599/// Possible Duplicate Message structure for PDM tag
600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
601pub struct PossibleDuplicateMessage {
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub time: Option<String>, // HHMM (optional)
604
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub message_output_reference: Option<MessageOutputReference>, // MOR (optional)
607}
608
609/// Message Output Reference structure (similar to MIR but for output)
610#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
611pub struct MessageOutputReference {
612    pub date: String,            // YYMMDD
613    pub lt_identifier: String,   // 12 characters
614    pub branch_code: String,     // 3!c
615    pub session_number: String,  // 4!n
616    pub sequence_number: String, // 6!n
617}
618
619/// System Originated Message structure for SYS tag
620#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
621pub struct SystemOriginatedMessage {
622    #[serde(skip_serializing_if = "Option::is_none")]
623    pub time: Option<String>, // HHMM (optional)
624
625    #[serde(skip_serializing_if = "Option::is_none")]
626    pub message_input_reference: Option<MessageInputReference>, // MIR (optional)
627}
628
629impl Trailer {
630    /// Parse trailer from block 5 string using structured parsing
631    pub fn parse(block5: &str) -> Result<Self> {
632        let mut trailer = Trailer::default();
633
634        // Extract common tags if present
635        if block5.contains("{CHK:") {
636            if let Some(start) = block5.find("{CHK:") {
637                if let Some(end) = block5[start..].find('}') {
638                    trailer.checksum = Some(block5[start + 5..start + end].to_string());
639                }
640            }
641        }
642
643        if block5.contains("{TNG}") {
644            trailer.test_and_training = Some(true);
645        }
646
647        if block5.contains("{DLM}") {
648            trailer.delayed_message = Some(true);
649        }
650
651        if block5.contains("{MAC:") {
652            if let Some(start) = block5.find("{MAC:") {
653                if let Some(end) = block5[start..].find('}') {
654                    trailer.mac = Some(block5[start + 5..start + end].to_string());
655                }
656            }
657        }
658
659        // More complex parsing for structured tags can be added here
660        // For now, implementing basic tag extraction
661
662        Ok(trailer)
663    }
664}
665
666impl std::fmt::Display for Trailer {
667    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
668        let mut result = String::new();
669
670        if let Some(ref checksum) = self.checksum {
671            result.push_str(&format!("{{CHK:{checksum}}}"));
672        }
673
674        if let Some(true) = self.test_and_training {
675            result.push_str("{TNG}");
676        }
677
678        if let Some(true) = self.delayed_message {
679            result.push_str("{DLM}");
680        }
681
682        if let Some(ref possible_duplicate_emission) = self.possible_duplicate_emission {
683            result.push_str(&format!(
684                "{{PDE:{}}}",
685                possible_duplicate_emission.time.as_deref().unwrap_or("")
686            ));
687        }
688
689        if let Some(ref message_reference) = self.message_reference {
690            result.push_str(&format!("{{MRF:{}}}", message_reference.date));
691        }
692
693        if let Some(ref mac) = self.mac {
694            result.push_str(&format!("{{MAC:{mac}}}"));
695        }
696
697        write!(f, "{result}")
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    #[test]
706    fn test_basic_header_parse() {
707        let block1 = "F01BANKDEFFAXXX0123456789";
708        let header = BasicHeader::parse(block1).unwrap();
709
710        assert_eq!(header.application_id, "F");
711        assert_eq!(header.service_id, "01");
712        assert_eq!(header.logical_terminal, "BANKDEFFAXXX");
713        assert_eq!(header.session_number, "0123");
714        assert_eq!(header.sequence_number, "456789");
715    }
716
717    #[test]
718    fn test_application_header_parse() {
719        let block2 = "I103BANKDEFFAXXXU3003";
720        let header = ApplicationHeader::parse(block2).unwrap();
721
722        assert_eq!(header.direction, "I");
723        assert_eq!(header.message_type, "103");
724        assert_eq!(header.destination_address, "BANKDEFFAXXX");
725        assert_eq!(header.priority, "U");
726        assert_eq!(header.delivery_monitoring, Some("3".to_string()));
727        assert_eq!(header.obsolescence_period, Some("003".to_string()));
728    }
729}