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!(
48 "Failed to parse BIC from logical terminal '{}': {}",
49 bic_str, e
50 ),
51 })?;
52
53 Ok(BasicHeader {
54 application_id,
55 service_id,
56 logical_terminal,
57 sender_bic,
58 session_number,
59 sequence_number,
60 })
61 }
62}
63
64impl std::fmt::Display for BasicHeader {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(
67 f,
68 "{}{}{}{}{}",
69 self.application_id,
70 self.service_id,
71 self.logical_terminal,
72 self.session_number,
73 self.sequence_number
74 )
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
80pub struct ApplicationHeader {
81 pub direction: String,
83
84 pub message_type: String,
86
87 pub destination_address: String,
89 pub receiver_bic: BIC,
90
91 pub priority: String,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub delivery_monitoring: Option<String>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub obsolescence_period: Option<String>,
101}
102
103impl ApplicationHeader {
104 pub fn parse(block2: &str) -> Result<Self> {
106 if block2.len() < 17 {
107 return Err(ParseError::InvalidBlockStructure {
108 message: format!(
109 "Block 2 too short: expected at least 18 characters, got {}",
110 block2.len()
111 ),
112 });
113 }
114
115 let direction = block2[0..1].to_string();
116 let message_type = block2[1..4].to_string();
117 let destination_address = block2[4..16].to_string();
118 let priority = block2[16..17].to_string();
119
120 let delivery_monitoring = if block2.len() >= 18 {
121 Some(block2[17..18].to_string())
122 } else {
123 None
124 };
125
126 let obsolescence_period = if block2.len() >= 21 {
127 Some(block2[18..21].to_string())
128 } else {
129 None
130 };
131
132 let receiver_bic = BIC::from_str(&destination_address[0..8]).map_err(|e| {
133 ParseError::InvalidBlockStructure {
134 message: format!(
135 "Failed to parse BIC from destination address '{}': {}",
136 destination_address, e
137 ),
138 }
139 })?;
140
141 Ok(ApplicationHeader {
142 direction,
143 message_type,
144 destination_address,
145 receiver_bic,
146 priority,
147 delivery_monitoring,
148 obsolescence_period,
149 })
150 }
151}
152
153impl std::fmt::Display for ApplicationHeader {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 let mut result = format!(
156 "{}{}{}{}",
157 self.direction, self.message_type, self.destination_address, self.priority
158 );
159
160 if let Some(ref delivery_monitoring) = self.delivery_monitoring {
161 result.push_str(delivery_monitoring);
162 }
163
164 if let Some(ref obsolescence_period) = self.obsolescence_period {
165 result.push_str(obsolescence_period);
166 }
167
168 write!(f, "{}", result)
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
174pub struct UserHeader {
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub service_identifier: Option<String>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub banking_priority: Option<String>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub message_user_reference: Option<String>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub validation_flag: Option<String>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub balance_checkpoint: Option<BalanceCheckpoint>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub message_input_reference: Option<MessageInputReference>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub related_reference: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub service_type_identifier: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub unique_end_to_end_reference: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub addressee_information: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub payment_release_information: Option<PaymentReleaseInfo>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub payment_controls_info: Option<PaymentControlsInfo>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230pub struct BalanceCheckpoint {
231 pub date: String, pub time: String, #[serde(skip_serializing_if = "Option::is_none")]
235 pub hundredths_of_second: Option<String>, }
237
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
240pub struct MessageInputReference {
241 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
250pub struct PaymentReleaseInfo {
251 pub code: String, #[serde(skip_serializing_if = "Option::is_none")]
254 pub additional_info: Option<String>, }
256
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259pub struct SanctionsScreeningInfo {
260 pub code_word: String, #[serde(skip_serializing_if = "Option::is_none")]
263 pub additional_info: Option<String>, }
265
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
268pub struct PaymentControlsInfo {
269 pub code_word: String, #[serde(skip_serializing_if = "Option::is_none")]
272 pub additional_info: Option<String>, }
274
275impl UserHeader {
276 pub fn parse(block3: &str) -> Result<Self> {
278 let mut user_header = UserHeader::default();
279
280 if block3.contains("{103:") {
283 if let Some(start) = block3.find("{103:") {
284 if let Some(end) = block3[start..].find('}') {
285 user_header.service_identifier =
286 Some(block3[start + 5..start + end].to_string());
287 }
288 }
289 }
290
291 if block3.contains("{113:") {
292 if let Some(start) = block3.find("{113:") {
293 if let Some(end) = block3[start..].find('}') {
294 user_header.banking_priority = Some(block3[start + 5..start + end].to_string());
295 }
296 }
297 }
298
299 if block3.contains("{108:") {
300 if let Some(start) = block3.find("{108:") {
301 if let Some(end) = block3[start..].find('}') {
302 user_header.message_user_reference =
303 Some(block3[start + 5..start + end].to_string());
304 }
305 }
306 }
307
308 if block3.contains("{119:") {
309 if let Some(start) = block3.find("{119:") {
310 if let Some(end) = block3[start..].find('}') {
311 user_header.validation_flag = Some(block3[start + 5..start + end].to_string());
312 }
313 }
314 }
315
316 if block3.contains("{423:") {
317 if let Some(start) = block3.find("{423:") {
318 if let Some(end) = block3[start..].find('}') {
319 let value = &block3[start + 5..start + end];
320 user_header.balance_checkpoint = Self::parse_balance_checkpoint(value);
321 }
322 }
323 }
324
325 if block3.contains("{106:") {
326 if let Some(start) = block3.find("{106:") {
327 if let Some(end) = block3[start..].find('}') {
328 let value = &block3[start + 5..start + end];
329 user_header.message_input_reference =
330 Self::parse_message_input_reference(value);
331 }
332 }
333 }
334
335 if block3.contains("{424:") {
336 if let Some(start) = block3.find("{424:") {
337 if let Some(end) = block3[start..].find('}') {
338 user_header.related_reference =
339 Some(block3[start + 5..start + end].to_string());
340 }
341 }
342 }
343
344 if block3.contains("{111:") {
345 if let Some(start) = block3.find("{111:") {
346 if let Some(end) = block3[start..].find('}') {
347 user_header.service_type_identifier =
348 Some(block3[start + 5..start + end].to_string());
349 }
350 }
351 }
352
353 if block3.contains("{121:") {
354 if let Some(start) = block3.find("{121:") {
355 if let Some(end) = block3[start..].find('}') {
356 user_header.unique_end_to_end_reference =
357 Some(block3[start + 5..start + end].to_string());
358 }
359 }
360 }
361
362 if block3.contains("{115:") {
363 if let Some(start) = block3.find("{115:") {
364 if let Some(end) = block3[start..].find('}') {
365 user_header.addressee_information =
366 Some(block3[start + 5..start + end].to_string());
367 }
368 }
369 }
370
371 if block3.contains("{165:") {
372 if let Some(start) = block3.find("{165:") {
373 if let Some(end) = block3[start..].find('}') {
374 let value = &block3[start + 5..start + end];
375 user_header.payment_release_information =
376 Self::parse_payment_release_info(value);
377 }
378 }
379 }
380
381 if block3.contains("{433:") {
382 if let Some(start) = block3.find("{433:") {
383 if let Some(end) = block3[start..].find('}') {
384 let value = &block3[start + 5..start + end];
385 user_header.sanctions_screening_info =
386 Self::parse_sanctions_screening_info(value);
387 }
388 }
389 }
390
391 if block3.contains("{434:") {
392 if let Some(start) = block3.find("{434:") {
393 if let Some(end) = block3[start..].find('}') {
394 let value = &block3[start + 5..start + end];
395 user_header.payment_controls_info = Self::parse_payment_controls_info(value);
396 }
397 }
398 }
399
400 Ok(user_header)
401 }
402
403 fn parse_balance_checkpoint(value: &str) -> Option<BalanceCheckpoint> {
405 if value.len() >= 12 {
406 Some(BalanceCheckpoint {
407 date: value[0..6].to_string(),
408 time: value[6..12].to_string(),
409 hundredths_of_second: if value.len() > 12 {
410 Some(value[12..].to_string())
411 } else {
412 None
413 },
414 })
415 } else {
416 None
417 }
418 }
419
420 fn parse_message_input_reference(value: &str) -> Option<MessageInputReference> {
422 if value.len() >= 28 {
423 Some(MessageInputReference {
424 date: value[0..6].to_string(),
425 lt_identifier: value[6..18].to_string(),
426 branch_code: value[18..21].to_string(),
427 session_number: value[21..25].to_string(),
428 sequence_number: value[25..].to_string(),
429 })
430 } else {
431 None
432 }
433 }
434
435 fn parse_payment_release_info(value: &str) -> Option<PaymentReleaseInfo> {
437 if value.len() >= 3 {
438 let code = value[0..3].to_string();
439 let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
440 Some(value[4..].to_string())
441 } else {
442 None
443 };
444 Some(PaymentReleaseInfo {
445 code,
446 additional_info,
447 })
448 } else {
449 None
450 }
451 }
452
453 fn parse_sanctions_screening_info(value: &str) -> Option<SanctionsScreeningInfo> {
455 if value.len() >= 3 {
456 let code_word = value[0..3].to_string();
457 let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
458 Some(value[4..].to_string())
459 } else {
460 None
461 };
462 Some(SanctionsScreeningInfo {
463 code_word,
464 additional_info,
465 })
466 } else {
467 None
468 }
469 }
470
471 fn parse_payment_controls_info(value: &str) -> Option<PaymentControlsInfo> {
473 if value.len() >= 3 {
474 let code_word = value[0..3].to_string();
475 let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
476 Some(value[4..].to_string())
477 } else {
478 None
479 };
480 Some(PaymentControlsInfo {
481 code_word,
482 additional_info,
483 })
484 } else {
485 None
486 }
487 }
488}
489
490impl std::fmt::Display for UserHeader {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 let mut result = String::new();
493
494 if let Some(ref service_id) = self.service_identifier {
495 result.push_str(&format!("{{103:{}}}", service_id));
496 }
497
498 if let Some(ref banking_priority) = self.banking_priority {
499 result.push_str(&format!("{{113:{}}}", banking_priority));
500 }
501
502 if let Some(ref message_user_ref) = self.message_user_reference {
503 result.push_str(&format!("{{108:{}}}", message_user_ref));
504 }
505
506 if let Some(ref validation_flag) = self.validation_flag {
507 result.push_str(&format!("{{119:{}}}", validation_flag));
508 }
509
510 if let Some(ref unique_end_to_end_ref) = self.unique_end_to_end_reference {
511 result.push_str(&format!("{{121:{}}}", unique_end_to_end_ref));
512 }
513
514 if let Some(ref service_type_id) = self.service_type_identifier {
515 result.push_str(&format!("{{111:{}}}", service_type_id));
516 }
517
518 if let Some(ref payment_controls) = self.payment_controls_info {
519 let mut value = payment_controls.code_word.clone();
520 if let Some(ref additional) = payment_controls.additional_info {
521 value.push('/');
522 value.push_str(additional);
523 }
524 result.push_str(&format!("{{434:{}}}", value));
525 }
526
527 if let Some(ref payment_release) = self.payment_release_information {
528 let mut value = payment_release.code.clone();
529 if let Some(ref additional) = payment_release.additional_info {
530 value.push('/');
531 value.push_str(additional);
532 }
533 result.push_str(&format!("{{165:{}}}", value));
534 }
535
536 if let Some(ref sanctions) = self.sanctions_screening_info {
537 let mut value = sanctions.code_word.clone();
538 if let Some(ref additional) = sanctions.additional_info {
539 value.push('/');
540 value.push_str(additional);
541 }
542 result.push_str(&format!("{{433:{}}}", value));
543 }
544
545 write!(f, "{}", result)
546 }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
551pub struct Trailer {
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub checksum: Option<String>,
555
556 #[serde(skip_serializing_if = "Option::is_none")]
558 pub test_and_training: Option<bool>,
559
560 #[serde(skip_serializing_if = "Option::is_none")]
562 pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
563
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub delayed_message: Option<bool>,
567
568 #[serde(skip_serializing_if = "Option::is_none")]
570 pub message_reference: Option<MessageReference>,
571
572 #[serde(skip_serializing_if = "Option::is_none")]
574 pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
575
576 #[serde(skip_serializing_if = "Option::is_none")]
578 pub system_originated_message: Option<SystemOriginatedMessage>,
579
580 #[serde(skip_serializing_if = "Option::is_none")]
582 pub mac: Option<String>,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
587pub struct PossibleDuplicateEmission {
588 #[serde(skip_serializing_if = "Option::is_none")]
589 pub time: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
592 pub message_input_reference: Option<MessageInputReference>, }
594
595#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
597pub struct MessageReference {
598 pub date: String, pub full_time: String, pub message_input_reference: MessageInputReference, }
602
603#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
605pub struct PossibleDuplicateMessage {
606 #[serde(skip_serializing_if = "Option::is_none")]
607 pub time: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
610 pub message_output_reference: Option<MessageOutputReference>, }
612
613#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
615pub struct MessageOutputReference {
616 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
622
623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
625pub struct SystemOriginatedMessage {
626 #[serde(skip_serializing_if = "Option::is_none")]
627 pub time: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
630 pub message_input_reference: Option<MessageInputReference>, }
632
633impl Trailer {
634 pub fn parse(block5: &str) -> Result<Self> {
636 let mut trailer = Trailer::default();
637
638 if block5.contains("{CHK:") {
640 if let Some(start) = block5.find("{CHK:") {
641 if let Some(end) = block5[start..].find('}') {
642 trailer.checksum = Some(block5[start + 5..start + end].to_string());
643 }
644 }
645 }
646
647 if block5.contains("{TNG}") {
648 trailer.test_and_training = Some(true);
649 }
650
651 if block5.contains("{DLM}") {
652 trailer.delayed_message = Some(true);
653 }
654
655 if block5.contains("{MAC:") {
656 if let Some(start) = block5.find("{MAC:") {
657 if let Some(end) = block5[start..].find('}') {
658 trailer.mac = Some(block5[start + 5..start + end].to_string());
659 }
660 }
661 }
662
663 Ok(trailer)
667 }
668}
669
670impl std::fmt::Display for Trailer {
671 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672 let mut result = String::new();
673
674 if let Some(ref checksum) = self.checksum {
675 result.push_str(&format!("{{CHK:{}}}", checksum));
676 }
677
678 if let Some(true) = self.test_and_training {
679 result.push_str("{TNG}");
680 }
681
682 if let Some(true) = self.delayed_message {
683 result.push_str("{DLM}");
684 }
685
686 if let Some(ref possible_duplicate_emission) = self.possible_duplicate_emission {
687 result.push_str(&format!(
688 "{{PDE:{}}}",
689 possible_duplicate_emission.time.as_deref().unwrap_or("")
690 ));
691 }
692
693 if let Some(ref message_reference) = self.message_reference {
694 result.push_str(&format!("{{MRF:{}}}", message_reference.date));
695 }
696
697 if let Some(ref mac) = self.mac {
698 result.push_str(&format!("{{MAC:{}}}", mac));
699 }
700
701 write!(f, "{}", result)
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn test_basic_header_parse() {
711 let block1 = "F01BANKDEFFAXXX0123456789";
712 let header = BasicHeader::parse(block1).unwrap();
713
714 assert_eq!(header.application_id, "F");
715 assert_eq!(header.service_id, "01");
716 assert_eq!(header.logical_terminal, "BANKDEFFAXXX");
717 assert_eq!(header.session_number, "0123");
718 assert_eq!(header.sequence_number, "456789");
719 }
720
721 #[test]
722 fn test_application_header_parse() {
723 let block2 = "I103BANKDEFFAXXXU3003";
724 let header = ApplicationHeader::parse(block2).unwrap();
725
726 assert_eq!(header.direction, "I");
727 assert_eq!(header.message_type, "103");
728 assert_eq!(header.destination_address, "BANKDEFFAXXX");
729 assert_eq!(header.priority, "U");
730 assert_eq!(header.delivery_monitoring, Some("3".to_string()));
731 assert_eq!(header.obsolescence_period, Some("003".to_string()));
732 }
733}