swift_mt_message/headers/
mod.rs

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