1use crate::errors::{ParseError, Result};
2use crate::fields::common::BIC;
3use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct BasicHeader {
9 pub application_id: String,
11
12 pub service_id: String,
14
15 pub logical_terminal: String,
17 pub sender_bic: BIC,
18
19 pub session_number: String,
21
22 pub sequence_number: String,
24}
25
26impl BasicHeader {
27 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct ApplicationHeader {
78 pub direction: String,
80
81 pub message_type: String,
83
84 pub destination_address: String,
86 pub receiver_bic: BIC,
87
88 pub priority: String,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub delivery_monitoring: Option<String>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub obsolescence_period: Option<String>,
98}
99
100impl ApplicationHeader {
101 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
170pub struct UserHeader {
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub service_identifier: Option<String>,
174
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub banking_priority: Option<String>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub message_user_reference: Option<String>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub validation_flag: Option<String>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub balance_checkpoint: Option<BalanceCheckpoint>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub message_input_reference: Option<MessageInputReference>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub related_reference: Option<String>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub service_type_identifier: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub unique_end_to_end_reference: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub addressee_information: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub payment_release_information: Option<PaymentReleaseInfo>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub payment_controls_info: Option<PaymentControlsInfo>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
226pub struct BalanceCheckpoint {
227 pub date: String, pub time: String, #[serde(skip_serializing_if = "Option::is_none")]
231 pub hundredths_of_second: Option<String>, }
233
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
236pub struct MessageInputReference {
237 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
246pub struct PaymentReleaseInfo {
247 pub code: String, #[serde(skip_serializing_if = "Option::is_none")]
250 pub additional_info: Option<String>, }
252
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
255pub struct SanctionsScreeningInfo {
256 pub code_word: String, #[serde(skip_serializing_if = "Option::is_none")]
259 pub additional_info: Option<String>, }
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264pub struct PaymentControlsInfo {
265 pub code_word: String, #[serde(skip_serializing_if = "Option::is_none")]
268 pub additional_info: Option<String>, }
270
271impl UserHeader {
272 pub fn parse(block3: &str) -> Result<Self> {
274 let mut user_header = UserHeader::default();
275
276 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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
547pub struct Trailer {
548 #[serde(skip_serializing_if = "Option::is_none")]
550 pub checksum: Option<String>,
551
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub test_and_training: Option<bool>,
555
556 #[serde(skip_serializing_if = "Option::is_none")]
558 pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
559
560 #[serde(skip_serializing_if = "Option::is_none")]
562 pub delayed_message: Option<bool>,
563
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub message_reference: Option<MessageReference>,
567
568 #[serde(skip_serializing_if = "Option::is_none")]
570 pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
571
572 #[serde(skip_serializing_if = "Option::is_none")]
574 pub system_originated_message: Option<SystemOriginatedMessage>,
575
576 #[serde(skip_serializing_if = "Option::is_none")]
578 pub mac: Option<String>,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
583pub struct PossibleDuplicateEmission {
584 #[serde(skip_serializing_if = "Option::is_none")]
585 pub time: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
588 pub message_input_reference: Option<MessageInputReference>, }
590
591#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
593pub struct MessageReference {
594 pub date: String, pub full_time: String, pub message_input_reference: MessageInputReference, }
598
599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
601pub struct PossibleDuplicateMessage {
602 #[serde(skip_serializing_if = "Option::is_none")]
603 pub time: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
606 pub message_output_reference: Option<MessageOutputReference>, }
608
609#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
611pub struct MessageOutputReference {
612 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
618
619#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
621pub struct SystemOriginatedMessage {
622 #[serde(skip_serializing_if = "Option::is_none")]
623 pub time: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
626 pub message_input_reference: Option<MessageInputReference>, }
628
629impl Trailer {
630 pub fn parse(block5: &str) -> Result<Self> {
632 let mut trailer = Trailer::default();
633
634 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 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}