1use once_cell::sync::Lazy;
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10
11use crate::errors::{ParseError, Result};
12
13#[derive(Default, Debug, Clone, Serialize, Deserialize)]
15pub struct SwiftMessageBlocks {
16 pub block_1: Option<String>,
17 pub block_2: Option<String>,
18 pub block_3: Option<String>,
19 pub block_4: Option<String>,
20 pub block_5: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ParsedField {
26 pub tag: String, pub content: String, pub line_number: usize, pub column: usize, }
31
32static FIELD_REGEX: Lazy<Regex> =
34 Lazy::new(|| Regex::new(r":(\d{2}[A-Z]?):").expect("Invalid field regex"));
35
36pub fn extract_blocks(message: &str) -> Result<SwiftMessageBlocks> {
38 let mut blocks = SwiftMessageBlocks::default();
39 let chars: Vec<char> = message.chars().collect();
40 let mut i = 0;
41
42 while i < chars.len() {
43 if chars[i] == '{' && i + 2 < chars.len() && chars[i + 2] == ':' {
44 let block_number = chars[i + 1];
46
47 if block_number.is_ascii_digit() {
48 let mut brace_count = 1;
50 let content_start = i + 3; let mut j = content_start;
52
53 while j < chars.len() && brace_count > 0 {
54 match chars[j] {
55 '{' => brace_count += 1,
56 '}' => brace_count -= 1,
57 _ => {}
58 }
59 j += 1;
60 }
61
62 if brace_count == 0 {
63 let content_end = j - 1; let block_content: String = chars[content_start..content_end].iter().collect();
66
67 match block_number {
68 '1' => blocks.block_1 = Some(block_content),
69 '2' => blocks.block_2 = Some(block_content),
70 '3' => blocks.block_3 = Some(block_content),
71 '4' => blocks.block_4 = Some(block_content),
72 '5' => blocks.block_5 = Some(block_content),
73 _ => {
74 return Err(ParseError::UnknownBlockNumber {
75 block_number: block_number.to_string(),
76 line: 1, column: i + 1,
78 });
79 }
80 }
81
82 i = j; } else {
84 return Err(ParseError::InvalidBlockFormat {
85 message: format!("Unmatched opening brace for block {}", block_number),
86 line: 1,
87 column: i + 1,
88 });
89 }
90 } else {
91 i += 1;
92 }
93 } else {
94 i += 1;
95 }
96 }
97
98 if blocks.block_1.is_none() && blocks.block_2.is_none() && blocks.block_4.is_none() {
99 return Err(ParseError::NoBlocksFound {
100 message: "No valid SWIFT blocks found in message".to_string(),
101 });
102 }
103
104 Ok(blocks)
105}
106
107pub fn parse_block4_fields(block4_content: &str) -> Result<Vec<ParsedField>> {
109 if block4_content.is_empty() {
110 return Ok(Vec::new());
111 }
112
113 let mut fields = Vec::new();
114 let _current_line = 1usize;
115 let lines = block4_content.lines().enumerate();
116
117 let mut field_positions = Vec::new();
119
120 for (line_idx, line) in lines {
121 if let Some(captures) = FIELD_REGEX.captures(line) {
122 if let Some(tag_match) = captures.get(1) {
123 field_positions.push((
124 tag_match.as_str().to_string(),
125 line_idx + 1,
126 tag_match.start(),
127 ));
128 }
129 }
130 }
131
132 if field_positions.is_empty() {
133 return Ok(Vec::new());
134 }
135
136 let content_lines: Vec<&str> = block4_content.lines().collect();
138
139 for (i, (tag, line_number, column)) in field_positions.iter().enumerate() {
140 let start_line = line_number - 1; let end_line = if i + 1 < field_positions.len() {
142 field_positions[i + 1].1 - 1 } else {
144 content_lines.len() };
146
147 let mut field_content = String::new();
149
150 if start_line < content_lines.len() {
152 let first_line = content_lines[start_line];
153 if let Some(tag_end) = first_line.find(&format!(":{}:", tag)) {
154 let content_start = tag_end + tag.len() + 2; if content_start < first_line.len() {
156 field_content.push_str(&first_line[content_start..]);
157 }
158 }
159 }
160
161 for line_idx in (start_line + 1)..end_line {
163 if line_idx < content_lines.len() {
164 if !field_content.is_empty() {
165 field_content.push('\n');
166 }
167 field_content.push_str(content_lines[line_idx]);
168 }
169 }
170
171 let cleaned_content = field_content
173 .trim()
174 .trim_end_matches('-') .trim() .to_string();
177
178 fields.push(ParsedField {
179 tag: tag.clone(),
180 content: cleaned_content,
181 line_number: *line_number,
182 column: *column,
183 });
184 }
185
186 Ok(fields)
187}
188
189pub fn extract_message_type(blocks: &SwiftMessageBlocks) -> Result<String> {
191 let block2 = blocks
192 .block_2
193 .as_ref()
194 .ok_or_else(|| ParseError::MissingRequiredBlock {
195 block: "2".to_string(),
196 message: "Block 2 is required to determine message type".to_string(),
197 line: 1,
198 column: 1,
199 })?;
200
201 if block2.len() < 4 {
202 return Err(ParseError::InvalidBlockFormat {
203 message: "Block 2 too short to contain message type".to_string(),
204 line: 1,
205 column: 1,
206 });
207 }
208
209 Ok(block2[1..4].to_string())
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
215pub struct BasicHeader {
216 pub application_id: String, pub service_id: String, pub logical_terminal: String, pub session_number: String, pub sequence_number: String, }
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225pub struct ApplicationHeader {
226 pub input_output_identifier: String, pub message_type: String, pub destination_address: String, pub priority: String, pub delivery_monitoring: Option<String>, pub obsolescence_period: Option<String>, }
233
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
236pub struct UserHeader {
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub service_identifier: Option<String>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub banking_priority: Option<String>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub message_user_reference: Option<String>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub validation_flag: Option<String>,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub balance_checkpoint: Option<BalanceCheckpoint>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub message_input_reference: Option<MessageInputReference>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub related_reference: Option<String>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub service_type_identifier: Option<String>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub unique_end_to_end_reference: Option<String>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub addressee_information: Option<String>,
276
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub payment_release_information: Option<PaymentReleaseInfo>,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
284
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub payment_controls_info: Option<PaymentControlsInfo>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
292pub struct BalanceCheckpoint {
293 pub date: String, pub time: String, pub hundredths_of_second: Option<String>, }
297
298#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
300pub struct MessageInputReference {
301 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
310pub struct PaymentReleaseInfo {
311 pub code: String, pub additional_info: Option<String>, }
314
315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub struct SanctionsScreeningInfo {
318 pub code_word: String, pub additional_info: Option<String>, }
321
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
324pub struct PaymentControlsInfo {
325 pub code_word: String, pub additional_info: Option<String>, }
328
329#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
331pub struct Trailer {
332 pub checksum: String,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub test_and_training: Option<bool>,
338
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
342
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub delayed_message: Option<bool>,
346
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub message_reference: Option<MessageReference>,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub system_originated_message: Option<SystemOriginatedMessage>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
362pub struct PossibleDuplicateEmission {
363 pub time: Option<String>, pub message_input_reference: Option<MessageInputReference>, }
366
367#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
369pub struct MessageReference {
370 pub date: String, pub full_time: String, pub message_input_reference: MessageInputReference, }
374
375#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
377pub struct PossibleDuplicateMessage {
378 pub time: Option<String>, pub message_output_reference: Option<MessageOutputReference>, }
381
382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
384pub struct MessageOutputReference {
385 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
391
392#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
394pub struct SystemOriginatedMessage {
395 pub time: Option<String>, pub message_input_reference: Option<MessageInputReference>, }
398
399pub fn parse_basic_header(block1_content: &str) -> Result<BasicHeader> {
401 if block1_content.len() < 21 {
402 return Err(ParseError::InvalidBlockFormat {
403 message: format!(
404 "Basic header block too short: expected at least 21 characters, got {}",
405 block1_content.len()
406 ),
407 line: 1,
408 column: 1,
409 });
410 }
411
412 Ok(BasicHeader {
413 application_id: block1_content[0..1].to_string(),
414 service_id: block1_content[1..3].to_string(),
415 logical_terminal: block1_content[3..15].to_string(),
416 session_number: block1_content[15..19].to_string(),
417 sequence_number: block1_content[19..].to_string(),
418 })
419}
420
421pub fn parse_application_header(block2_content: &str) -> Result<ApplicationHeader> {
423 if block2_content.is_empty() {
424 return Err(ParseError::InvalidBlockFormat {
425 message: "Application header block is empty".to_string(),
426 line: 1,
427 column: 1,
428 });
429 }
430
431 if block2_content.len() < 4 {
432 return Err(ParseError::InvalidBlockFormat {
433 message: format!(
434 "Application header block too short: expected at least 4 characters, got {}",
435 block2_content.len()
436 ),
437 line: 1,
438 column: 1,
439 });
440 }
441
442 let input_output_identifier = block2_content[0..1].to_string();
443 let message_type = block2_content[1..4].to_string();
444 let remaining = &block2_content[4..];
445
446 let (destination_address, priority, delivery_monitoring, obsolescence_period) =
447 if input_output_identifier == "I" {
448 if remaining.len() >= 12 {
450 let dest = remaining[0..12].to_string();
451 let prio = remaining
452 .get(12..13)
453 .map(|s| s.to_string())
454 .unwrap_or_default();
455 let del_mon = remaining.get(13..14).map(|s| s.to_string());
456 let obs_per = remaining.get(14..17).map(|s| s.to_string());
457 (dest, prio, del_mon, obs_per)
458 } else {
459 (String::new(), String::new(), None, None)
460 }
461 } else if input_output_identifier == "O" {
462 (String::new(), String::new(), None, None) } else {
465 return Err(ParseError::InvalidBlockFormat {
466 message: format!("Invalid I/O identifier: {}", input_output_identifier),
467 line: 1,
468 column: 1,
469 });
470 };
471
472 Ok(ApplicationHeader {
473 input_output_identifier,
474 message_type,
475 destination_address,
476 priority,
477 delivery_monitoring,
478 obsolescence_period,
479 })
480}
481
482pub fn parse_user_header(block3_content: &str) -> Result<UserHeader> {
484 let mut user_header = UserHeader::default();
485
486 let tag_value_regex =
488 Regex::new(r"\{([^:]+):([^}]*)\}").map_err(|e| ParseError::RegexError {
489 message: e.to_string(),
490 })?;
491
492 for cap in tag_value_regex.captures_iter(block3_content) {
493 if let (Some(tag), Some(value)) = (cap.get(1), cap.get(2)) {
494 let tag_str = tag.as_str();
495 let value_str = value.as_str();
496
497 match tag_str {
498 "103" => user_header.service_identifier = Some(value_str.to_string()),
499 "113" => user_header.banking_priority = Some(value_str.to_string()),
500 "108" => user_header.message_user_reference = Some(value_str.to_string()),
501 "119" => user_header.validation_flag = Some(value_str.to_string()),
502 "423" => user_header.balance_checkpoint = parse_balance_checkpoint(value_str)?,
503 "106" => {
504 user_header.message_input_reference = parse_message_input_reference(value_str)?
505 }
506 "424" => user_header.related_reference = Some(value_str.to_string()),
507 "111" => user_header.service_type_identifier = Some(value_str.to_string()),
508 "121" => user_header.unique_end_to_end_reference = Some(value_str.to_string()),
509 "115" => user_header.addressee_information = Some(value_str.to_string()),
510 "165" => {
511 user_header.payment_release_information = parse_payment_release_info(value_str)?
512 }
513 "433" => {
514 user_header.sanctions_screening_info =
515 parse_sanctions_screening_info(value_str)?
516 }
517 "434" => {
518 user_header.payment_controls_info = parse_payment_controls_info(value_str)?
519 }
520 _ => {
521 }
523 }
524 }
525 }
526
527 Ok(user_header)
528}
529
530pub fn parse_trailer_block(block5_content: &str) -> Result<Trailer> {
532 let mut trailer = Trailer {
533 checksum: String::new(), ..Default::default()
535 };
536
537 let tag_value_regex =
539 Regex::new(r"\{([^:]+):([^}]*)\}").map_err(|e| ParseError::RegexError {
540 message: e.to_string(),
541 })?;
542
543 for cap in tag_value_regex.captures_iter(block5_content) {
544 if let (Some(tag), Some(value)) = (cap.get(1), cap.get(2)) {
545 let tag_str = tag.as_str();
546 let value_str = value.as_str();
547
548 match tag_str {
549 "CHK" => trailer.checksum = value_str.to_string(),
550 "TNG" => trailer.test_and_training = Some(true),
551 "PDE" => {
552 trailer.possible_duplicate_emission =
553 parse_possible_duplicate_emission(value_str)?
554 }
555 "DLM" => trailer.delayed_message = Some(true),
556 "MRF" => trailer.message_reference = parse_message_reference(value_str)?,
557 "PDM" => {
558 trailer.possible_duplicate_message =
559 parse_possible_duplicate_message(value_str)?
560 }
561 "SYS" => {
562 trailer.system_originated_message = parse_system_originated_message(value_str)?
563 }
564 _ => {
565 }
567 }
568 }
569 }
570
571 Ok(trailer)
572}
573
574fn parse_balance_checkpoint(value: &str) -> Result<Option<BalanceCheckpoint>> {
576 if value.is_empty() {
577 return Ok(None);
578 }
579
580 if value.len() < 12 {
581 return Ok(None); }
583
584 let date = value[0..6].to_string();
585 let time = value[6..12].to_string();
586 let hundredths = if value.len() > 12 {
587 Some(value[12..].to_string())
588 } else {
589 None
590 };
591
592 Ok(Some(BalanceCheckpoint {
593 date,
594 time,
595 hundredths_of_second: hundredths,
596 }))
597}
598
599fn parse_message_input_reference(value: &str) -> Result<Option<MessageInputReference>> {
601 if value.is_empty() || value.len() < 28 {
602 return Ok(None); }
604
605 Ok(Some(MessageInputReference {
606 date: value[0..6].to_string(),
607 lt_identifier: value[6..18].to_string(),
608 branch_code: value[18..21].to_string(),
609 session_number: value[21..25].to_string(),
610 sequence_number: value[25..].to_string(),
611 }))
612}
613
614fn parse_payment_release_info(value: &str) -> Result<Option<PaymentReleaseInfo>> {
616 if value.is_empty() {
617 return Ok(None);
618 }
619
620 if value.starts_with('/') && value.len() >= 4 {
622 let parts: Vec<&str> = value[1..].split('/').collect();
623 if !parts.is_empty() {
624 let code = parts[0].to_string();
625 let additional_info = if parts.len() > 1 && !parts[1].is_empty() {
626 Some(parts[1].to_string())
627 } else {
628 None
629 };
630 return Ok(Some(PaymentReleaseInfo {
631 code,
632 additional_info,
633 }));
634 }
635 }
636
637 Ok(None)
638}
639
640fn parse_sanctions_screening_info(value: &str) -> Result<Option<SanctionsScreeningInfo>> {
642 if value.is_empty() {
643 return Ok(None);
644 }
645
646 if value.starts_with('/') && value.len() >= 4 {
648 let parts: Vec<&str> = value[1..].split('/').collect();
649 if !parts.is_empty() {
650 let code_word = parts[0].to_string();
651 let additional_info = if parts.len() > 1 && !parts[1].is_empty() {
652 Some(parts[1].to_string())
653 } else {
654 None
655 };
656 return Ok(Some(SanctionsScreeningInfo {
657 code_word,
658 additional_info,
659 }));
660 }
661 }
662
663 Ok(None)
664}
665
666fn parse_payment_controls_info(value: &str) -> Result<Option<PaymentControlsInfo>> {
668 if value.is_empty() {
669 return Ok(None);
670 }
671
672 if value.starts_with('/') && value.len() >= 4 {
674 let parts: Vec<&str> = value[1..].split('/').collect();
675 if !parts.is_empty() {
676 let code_word = parts[0].to_string();
677 let additional_info = if parts.len() > 1 && !parts[1].is_empty() {
678 Some(parts[1].to_string())
679 } else {
680 None
681 };
682 return Ok(Some(PaymentControlsInfo {
683 code_word,
684 additional_info,
685 }));
686 }
687 }
688
689 Ok(None)
690}
691
692fn parse_possible_duplicate_emission(value: &str) -> Result<Option<PossibleDuplicateEmission>> {
694 if value.is_empty() {
695 return Ok(Some(PossibleDuplicateEmission {
696 time: None,
697 message_input_reference: None,
698 }));
699 }
700
701 let time = if value.len() >= 4 {
703 Some(value[0..4].to_string())
704 } else {
705 None
706 };
707
708 let mir = if value.len() > 4 {
709 parse_message_input_reference(&value[4..])?
710 } else {
711 None
712 };
713
714 Ok(Some(PossibleDuplicateEmission {
715 time,
716 message_input_reference: mir,
717 }))
718}
719
720fn parse_message_reference(value: &str) -> Result<Option<MessageReference>> {
722 if value.is_empty() || value.len() < 38 {
723 return Ok(None); }
725
726 let date = value[0..6].to_string();
727 let full_time = value[6..10].to_string();
728 let mir_part = &value[10..];
729
730 let mir =
731 parse_message_input_reference(mir_part)?.ok_or_else(|| ParseError::InvalidBlockFormat {
732 message: "Invalid MIR in MRF tag".to_string(),
733 line: 1,
734 column: 1,
735 })?;
736
737 Ok(Some(MessageReference {
738 date,
739 full_time,
740 message_input_reference: mir,
741 }))
742}
743
744fn parse_possible_duplicate_message(value: &str) -> Result<Option<PossibleDuplicateMessage>> {
746 if value.is_empty() {
747 return Ok(Some(PossibleDuplicateMessage {
748 time: None,
749 message_output_reference: None,
750 }));
751 }
752
753 let time = if value.len() >= 4 {
755 Some(value[0..4].to_string())
756 } else {
757 None
758 };
759
760 let mor = if value.len() > 4 {
761 parse_message_output_reference(&value[4..])?
762 } else {
763 None
764 };
765
766 Ok(Some(PossibleDuplicateMessage {
767 time,
768 message_output_reference: mor,
769 }))
770}
771
772fn parse_message_output_reference(value: &str) -> Result<Option<MessageOutputReference>> {
774 if value.is_empty() || value.len() < 28 {
775 return Ok(None); }
777
778 Ok(Some(MessageOutputReference {
779 date: value[0..6].to_string(),
780 lt_identifier: value[6..18].to_string(),
781 branch_code: value[18..21].to_string(),
782 session_number: value[21..25].to_string(),
783 sequence_number: value[25..].to_string(),
784 }))
785}
786
787fn parse_system_originated_message(value: &str) -> Result<Option<SystemOriginatedMessage>> {
789 if value.is_empty() {
790 return Ok(Some(SystemOriginatedMessage {
791 time: None,
792 message_input_reference: None,
793 }));
794 }
795
796 let time = if value.len() >= 4 {
798 Some(value[0..4].to_string())
799 } else {
800 None
801 };
802
803 let mir = if value.len() > 4 {
804 parse_message_input_reference(&value[4..])?
805 } else {
806 None
807 };
808
809 Ok(Some(SystemOriginatedMessage {
810 time,
811 message_input_reference: mir,
812 }))
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818
819 #[test]
820 fn test_extract_blocks() {
821 let message = "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{4:\n:20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n-}";
822
823 let blocks = extract_blocks(message).unwrap();
824
825 assert!(blocks.block_1.is_some());
826 assert_eq!(blocks.block_1.unwrap(), "F01BANKDEFFAXXX0123456789");
827
828 assert!(blocks.block_2.is_some());
829 assert_eq!(blocks.block_2.unwrap(), "I103BANKDEFFAXXXU3003");
830
831 assert!(blocks.block_4.is_some());
832 assert!(blocks.block_4.unwrap().contains(":20:FT21234567890"));
833 }
834
835 #[test]
836 fn test_extract_blocks_with_nested_braces() {
837 let message = "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{3:{113:SEPA}{108:MT103}{119:STP}}{4:\n:20:FT21234567890\n:23B:CRED\n-}{5:{MAC:12345678}{CHK:ABCDEF}}";
839
840 let blocks = extract_blocks(message).unwrap();
841
842 assert!(blocks.block_1.is_some());
843 assert_eq!(blocks.block_1.unwrap(), "F01BANKDEFFAXXX0123456789");
844
845 assert!(blocks.block_2.is_some());
846 assert_eq!(blocks.block_2.unwrap(), "I103BANKDEFFAXXXU3003");
847
848 assert!(blocks.block_3.is_some());
849 assert_eq!(blocks.block_3.unwrap(), "{113:SEPA}{108:MT103}{119:STP}");
850
851 assert!(blocks.block_4.is_some());
852 assert!(blocks.block_4.unwrap().contains(":20:FT21234567890"));
853
854 assert!(blocks.block_5.is_some());
855 assert_eq!(blocks.block_5.unwrap(), "{MAC:12345678}{CHK:ABCDEF}");
856 }
857
858 #[test]
859 fn test_extract_blocks_single_nested_brace() {
860 let message =
862 "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{3:{113:SEPA}}{4:\n:20:TEST\n-}";
863
864 let blocks = extract_blocks(message).unwrap();
865
866 assert!(blocks.block_3.is_some());
867 assert_eq!(blocks.block_3.unwrap(), "{113:SEPA}");
868 }
869
870 #[test]
871 fn test_parse_block4_fields() {
872 let block4 = ":20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n:50K:JOHN DOE\nACME CORP\n123 MAIN ST\n:59:JANE SMITH\nXYZ COMPANY";
873
874 let fields = parse_block4_fields(block4).unwrap();
875
876 assert_eq!(fields.len(), 5);
877 assert_eq!(fields[0].tag, "20");
878 assert_eq!(fields[0].content, "FT21234567890");
879 assert_eq!(fields[1].tag, "23B");
880 assert_eq!(fields[1].content, "CRED");
881 assert_eq!(fields[3].tag, "50K");
882 assert_eq!(fields[3].content, "JOHN DOE\nACME CORP\n123 MAIN ST");
883 }
884
885 #[test]
886 fn test_extract_message_type() {
887 let blocks = SwiftMessageBlocks {
888 block_1: Some("F01BANKDEFFAXXX0123456789".to_string()),
889 block_2: Some("I103BANKDEFFAXXXU3003".to_string()),
890 block_3: None,
891 block_4: Some(":20:TEST".to_string()),
892 block_5: None,
893 };
894
895 let message_type = extract_message_type(&blocks).unwrap();
896 assert_eq!(message_type, "103");
897 }
898
899 #[test]
900 fn test_parse_basic_header() {
901 let block1 = "F01BANKDEFFAXXX0123456789";
902 let header = parse_basic_header(block1).unwrap();
903
904 assert_eq!(header.application_id, "F");
905 assert_eq!(header.service_id, "01");
906 assert_eq!(header.logical_terminal, "BANKDEFFAXXX");
907 assert_eq!(header.session_number, "0123");
908 assert_eq!(header.sequence_number, "456789");
909 }
910
911 #[test]
912 fn test_parse_application_header() {
913 let block2 = "I103BANKDEFFAXXXU3003";
914 let header = parse_application_header(block2).unwrap();
915
916 assert_eq!(header.input_output_identifier, "I");
917 assert_eq!(header.message_type, "103");
918 assert_eq!(header.destination_address, "BANKDEFFAXXX");
919 assert_eq!(header.priority, "U");
920 }
921}