1use crate::errors::{ParseError, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub struct BasicHeader {
7 pub application_id: String,
9
10 pub service_id: String,
12
13 pub logical_terminal: String,
15
16 pub session_number: String,
18
19 pub sequence_number: String,
21}
22
23impl BasicHeader {
24 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 let mut logical_terminal = String::new();
40 let mut pos = 3;
41
42 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct ApplicationHeader {
89 pub direction: String,
91
92 pub message_type: String,
94
95 pub destination_address: String,
97
98 pub priority: String,
100
101 pub delivery_monitoring: Option<String>,
103
104 pub obsolescence_period: Option<String>,
106}
107
108impl ApplicationHeader {
109 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
169pub struct UserHeader {
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub service_identifier: Option<String>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub banking_priority: Option<String>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub message_user_reference: Option<String>,
181
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub validation_flag: Option<String>,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub balance_checkpoint: Option<BalanceCheckpoint>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub message_input_reference: Option<MessageInputReference>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub related_reference: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub service_type_identifier: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub unique_end_to_end_reference: Option<String>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub addressee_information: Option<String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub payment_release_information: Option<PaymentReleaseInfo>,
213
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub payment_controls_info: Option<PaymentControlsInfo>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225pub struct BalanceCheckpoint {
226 pub date: String, pub time: String, pub hundredths_of_second: Option<String>, }
230
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
233pub struct MessageInputReference {
234 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
240
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
243pub struct PaymentReleaseInfo {
244 pub code: String, pub additional_info: Option<String>, }
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
250pub struct SanctionsScreeningInfo {
251 pub code_word: String, pub additional_info: Option<String>, }
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
257pub struct PaymentControlsInfo {
258 pub code_word: String, pub additional_info: Option<String>, }
261
262impl UserHeader {
263 pub fn parse(block3: &str) -> Result<Self> {
265 let mut user_header = UserHeader::default();
266
267 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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
538pub struct Trailer {
539 #[serde(skip_serializing_if = "Option::is_none")]
541 pub checksum: Option<String>,
542
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub test_and_training: Option<bool>,
546
547 #[serde(skip_serializing_if = "Option::is_none")]
549 pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
550
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub delayed_message: Option<bool>,
554
555 #[serde(skip_serializing_if = "Option::is_none")]
557 pub message_reference: Option<MessageReference>,
558
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
562
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub system_originated_message: Option<SystemOriginatedMessage>,
566
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub mac: Option<String>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
574pub struct PossibleDuplicateEmission {
575 pub time: Option<String>, pub message_input_reference: Option<MessageInputReference>, }
578
579#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
581pub struct MessageReference {
582 pub date: String, pub full_time: String, pub message_input_reference: MessageInputReference, }
586
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
589pub struct PossibleDuplicateMessage {
590 pub time: Option<String>, pub message_output_reference: Option<MessageOutputReference>, }
593
594#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
596pub struct MessageOutputReference {
597 pub date: String, pub lt_identifier: String, pub branch_code: String, pub session_number: String, pub sequence_number: String, }
603
604#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
606pub struct SystemOriginatedMessage {
607 pub time: Option<String>, pub message_input_reference: Option<MessageInputReference>, }
610
611impl Trailer {
612 pub fn parse(block5: &str) -> Result<Self> {
614 let mut trailer = Trailer::default();
615
616 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 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}