swift_mt_message/fields/
field20.rs1use crate::SwiftField;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, SwiftField)]
85#[format("16x")]
86pub struct Field20 {
87 #[format("16x")]
104 pub transaction_reference: String,
105}
106
107impl Field20 {
108 pub fn new(transaction_reference: String) -> Self {
134 Self {
135 transaction_reference,
136 }
137 }
138
139 pub fn transaction_reference(&self) -> &str {
154 &self.transaction_reference
155 }
156
157 pub fn reference_pattern(&self) -> &'static str {
175 let ref_upper = self.transaction_reference.to_uppercase();
176
177 if ref_upper.starts_with("FT") {
178 "Wire Transfer"
179 } else if ref_upper.starts_with("TXN") {
180 "Transaction ID"
181 } else if ref_upper.starts_with("REF") {
182 "Reference ID"
183 } else if ref_upper.starts_with("CUST") {
184 "Customer Reference"
185 } else if ref_upper.starts_with("CLI") {
186 "Client Reference"
187 } else if self.is_date_based() {
188 "Date-based"
189 } else if ref_upper.chars().all(|c| c.is_ascii_digit()) {
190 "Numeric Sequential"
191 } else {
192 "Custom"
193 }
194 }
195
196 pub fn is_date_based(&self) -> bool {
214 let ref_str = &self.transaction_reference;
215
216 if ref_str.len() >= 8 {
218 let date_part = &ref_str[0..8];
219 if date_part.chars().all(|c| c.is_ascii_digit()) {
220 if let (Ok(year), Ok(month), Ok(day)) = (
222 date_part[0..4].parse::<u32>(),
223 date_part[4..6].parse::<u32>(),
224 date_part[6..8].parse::<u32>(),
225 ) {
226 return (2000..=2099).contains(&year)
227 && (1..=12).contains(&month)
228 && (1..=31).contains(&day);
229 }
230 }
231 }
232
233 false
234 }
235
236 pub fn sequence_number(&self) -> Option<u32> {
254 let ref_str = &self.transaction_reference;
255
256 let mut numeric_suffix = String::new();
258 for ch in ref_str.chars().rev() {
259 if ch.is_ascii_digit() {
260 numeric_suffix.insert(0, ch);
261 } else {
262 break;
263 }
264 }
265
266 if !numeric_suffix.is_empty() && numeric_suffix.len() <= 10 {
267 numeric_suffix.parse().ok()
268 } else {
269 None
270 }
271 }
272
273 pub fn description(&self) -> String {
288 let pattern = self.reference_pattern();
289 let length = self.transaction_reference.len();
290
291 let mut desc = format!(
292 "Transaction Reference: {} (Pattern: {}, Length: {})",
293 self.transaction_reference, pattern, length
294 );
295
296 if self.is_date_based() {
297 desc.push_str(", Date-based");
298 }
299
300 if let Some(seq) = self.sequence_number() {
301 desc.push_str(&format!(", Sequence: {}", seq));
302 }
303
304 desc
305 }
306
307 pub fn is_well_formed(&self) -> bool {
325 let ref_str = &self.transaction_reference;
326
327 if ref_str.len() < 3 {
329 return false;
330 }
331
332 let unique_chars: std::collections::HashSet<char> = ref_str.chars().collect();
334 if unique_chars.len() < 2 {
335 return false;
336 }
337
338 if !ref_str
340 .chars()
341 .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '/' | '.'))
342 {
343 return false;
344 }
345
346 true
347 }
348}
349
350impl std::fmt::Display for Field20 {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 write!(f, "{}", self.transaction_reference)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_field20_creation() {
362 let field = Field20::new("FT21234567890".to_string());
363 assert_eq!(field.transaction_reference(), "FT21234567890");
364 }
365
366 #[test]
367 fn test_field20_parse() {
368 let field = Field20::parse("FT21234567890").unwrap();
369 assert_eq!(field.transaction_reference, "FT21234567890");
370 }
371
372 #[test]
373 fn test_field20_parse_with_prefix() {
374 let field = Field20::parse(":20:FT21234567890").unwrap();
375 assert_eq!(field.transaction_reference, "FT21234567890");
376 }
377
378 #[test]
379 fn test_field20_to_swift_string() {
380 let field = Field20::new("FT21234567890".to_string());
381 assert_eq!(field.to_swift_string(), ":20:FT21234567890");
382 }
383
384 #[test]
385 fn test_field20_validation() {
386 let valid_field = Field20::new("FT12345".to_string());
387 let result = valid_field.validate();
388 assert!(result.is_valid);
389
390 let invalid_field = Field20::new("THIS_IS_TOO_LONG_FOR_FIELD20".to_string());
391 let result = invalid_field.validate();
392 assert!(!result.is_valid);
393 }
394
395 #[test]
396 fn test_field20_format_spec() {
397 assert_eq!(Field20::format_spec(), "16x");
398 }
399
400 #[test]
401 fn test_field20_reference_pattern_detection() {
402 let ft_ref = Field20::new("FT21234567890".to_string());
404 assert_eq!(ft_ref.reference_pattern(), "Wire Transfer");
405
406 let txn_ref = Field20::new("TXN0000012345".to_string());
408 assert_eq!(txn_ref.reference_pattern(), "Transaction ID");
409
410 let ref_ref = Field20::new("REF987654321".to_string());
412 assert_eq!(ref_ref.reference_pattern(), "Reference ID");
413
414 let cust_ref = Field20::new("CUST12345678".to_string());
416 assert_eq!(cust_ref.reference_pattern(), "Customer Reference");
417
418 let cli_ref = Field20::new("CLI0000012345".to_string());
420 assert_eq!(cli_ref.reference_pattern(), "Client Reference");
421
422 let date_ref = Field20::new("20241201001".to_string());
424 assert_eq!(date_ref.reference_pattern(), "Date-based");
425
426 let num_ref = Field20::new("123456789".to_string());
428 assert_eq!(num_ref.reference_pattern(), "Numeric Sequential");
429
430 let custom_ref = Field20::new("CUSTOM_REF".to_string());
432 assert_eq!(custom_ref.reference_pattern(), "Customer Reference"); }
434
435 #[test]
436 fn test_field20_case_insensitive_pattern_detection() {
437 let ft_lower = Field20::new("ft21234567890".to_string());
439 assert_eq!(ft_lower.reference_pattern(), "Wire Transfer");
440
441 let txn_lower = Field20::new("txn0000012345".to_string());
442 assert_eq!(txn_lower.reference_pattern(), "Transaction ID");
443
444 let mixed_case = Field20::new("Ft21234567890".to_string());
446 assert_eq!(mixed_case.reference_pattern(), "Wire Transfer");
447 }
448
449 #[test]
450 fn test_field20_date_based_detection() {
451 let date_ref1 = Field20::new("20241201001".to_string());
453 assert!(date_ref1.is_date_based());
454
455 let date_ref2 = Field20::new("20230315999".to_string());
456 assert!(date_ref2.is_date_based());
457
458 let date_ref3 = Field20::new("20221231ABC".to_string());
459 assert!(date_ref3.is_date_based());
460
461 let invalid_year = Field20::new("19991201001".to_string());
463 assert!(!invalid_year.is_date_based());
464
465 let invalid_month = Field20::new("20241301001".to_string());
466 assert!(!invalid_month.is_date_based());
467
468 let invalid_day = Field20::new("20241232001".to_string());
469 assert!(!invalid_day.is_date_based());
470
471 let too_short = Field20::new("2024120".to_string());
472 assert!(!too_short.is_date_based());
473
474 let non_numeric = Field20::new("FT241201001".to_string());
475 assert!(!non_numeric.is_date_based());
476 }
477
478 #[test]
479 fn test_field20_sequence_number_extraction() {
480 let seq_ref1 = Field20::new("FT001".to_string());
482 assert_eq!(seq_ref1.sequence_number(), Some(1));
483
484 let seq_ref2 = Field20::new("TXN123456789".to_string());
485 assert_eq!(seq_ref2.sequence_number(), Some(123456789));
486
487 let seq_ref3 = Field20::new("REF000000042".to_string());
488 assert_eq!(seq_ref3.sequence_number(), Some(42));
489
490 let no_seq1 = Field20::new("CUSTOMREF".to_string());
492 assert_eq!(no_seq1.sequence_number(), None);
493
494 let no_seq2 = Field20::new("FT_NO_NUM".to_string());
495 assert_eq!(no_seq2.sequence_number(), None);
496
497 let all_num = Field20::new("123456789".to_string());
499 assert_eq!(all_num.sequence_number(), Some(123456789));
500
501 let too_long = Field20::new("FT12345678901".to_string());
503 assert_eq!(too_long.sequence_number(), None);
504 }
505
506 #[test]
507 fn test_field20_description_generation() {
508 let ft_ref = Field20::new("FT21234567890".to_string());
510 let description = ft_ref.description();
511 assert!(description.contains("FT21234567890"));
512 assert!(description.contains("Wire Transfer"));
513 assert!(description.contains("Length: 13"));
514
515 let date_seq_ref = Field20::new("20241201001".to_string());
517 let description = date_seq_ref.description();
518 assert!(description.contains("Date-based"));
519 assert!(!description.contains("Sequence:")); let custom_ref = Field20::new("MYREF123".to_string());
523 let description = custom_ref.description();
524 assert!(description.contains("Custom"));
525 assert!(description.contains("Sequence: 123"));
526 }
527
528 #[test]
529 fn test_field20_well_formed_validation() {
530 let good_ref1 = Field20::new("FT21234567890".to_string());
532 assert!(good_ref1.is_well_formed());
533
534 let good_ref2 = Field20::new("TXN-123456".to_string());
535 assert!(good_ref2.is_well_formed());
536
537 let good_ref3 = Field20::new("REF_001.234".to_string());
538 assert!(good_ref3.is_well_formed());
539
540 let good_ref4 = Field20::new("CUST/12345".to_string());
541 assert!(good_ref4.is_well_formed());
542
543 let too_short = Field20::new("AB".to_string());
545 assert!(!too_short.is_well_formed());
546
547 let all_same = Field20::new("AAAAAAA".to_string());
548 assert!(!all_same.is_well_formed());
549
550 let invalid_chars = Field20::new("REF@123#".to_string());
551 assert!(!invalid_chars.is_well_formed());
552
553 let spaces = Field20::new("REF 123".to_string());
554 assert!(!spaces.is_well_formed());
555 }
556
557 #[test]
558 fn test_field20_display_formatting() {
559 let field = Field20::new("FT21234567890".to_string());
560 assert_eq!(format!("{}", field), "FT21234567890");
561
562 let field2 = Field20::new("TXN0000012345".to_string());
563 assert_eq!(format!("{}", field2), "TXN0000012345");
564 }
565
566 #[test]
567 fn test_field20_edge_cases() {
568 let min_ref = Field20::new("ABC".to_string());
570 assert_eq!(min_ref.transaction_reference(), "ABC");
571 assert!(min_ref.is_well_formed());
572
573 let max_ref = Field20::new("1234567890123456".to_string());
575 assert_eq!(max_ref.transaction_reference(), "1234567890123456");
576 assert_eq!(max_ref.transaction_reference().len(), 16);
577
578 let single_char = Field20::new("A".to_string());
580 assert!(!single_char.is_well_formed());
581
582 let empty_ref = Field20::new("".to_string());
584 assert!(!empty_ref.is_well_formed());
585 }
586
587 #[test]
588 fn test_field20_real_world_examples() {
589 let wire_ref = Field20::new("FT001".to_string());
591 assert_eq!(wire_ref.reference_pattern(), "Wire Transfer");
592 assert!(!wire_ref.is_date_based());
593 assert_eq!(wire_ref.sequence_number(), Some(1));
594 assert!(wire_ref.is_well_formed());
595
596 let sys_ref = Field20::new("TXN0000012345".to_string());
598 assert_eq!(sys_ref.reference_pattern(), "Transaction ID");
599 assert!(!sys_ref.is_date_based());
600 assert_eq!(sys_ref.sequence_number(), Some(12345));
601 assert!(sys_ref.is_well_formed());
602
603 let cust_ref = Field20::new("CUST123456789A".to_string());
605 assert_eq!(cust_ref.reference_pattern(), "Customer Reference");
606 assert!(!cust_ref.is_date_based());
607 assert_eq!(cust_ref.sequence_number(), None); assert!(cust_ref.is_well_formed());
609
610 let branch_ref = Field20::new("NYC001234567".to_string());
612 assert_eq!(branch_ref.reference_pattern(), "Custom");
613 assert!(!branch_ref.is_date_based());
614 assert_eq!(branch_ref.sequence_number(), Some(1234567));
615 assert!(branch_ref.is_well_formed());
616 }
617
618 #[test]
619 fn test_field20_serialization() {
620 let field = Field20::new("FT21234567890".to_string());
621
622 let json = serde_json::to_string(&field).unwrap();
624 let deserialized: Field20 = serde_json::from_str(&json).unwrap();
625
626 assert_eq!(field, deserialized);
627 assert_eq!(
628 field.transaction_reference(),
629 deserialized.transaction_reference()
630 );
631 }
632
633 #[test]
634 fn test_field20_pattern_edge_cases() {
635 let ft_min = Field20::new("FT1".to_string());
637 assert_eq!(ft_min.reference_pattern(), "Wire Transfer");
638
639 let txn_min = Field20::new("TXN".to_string());
640 assert_eq!(txn_min.reference_pattern(), "Transaction ID");
641
642 let date_edge1 = Field20::new("20000101".to_string());
644 assert!(date_edge1.is_date_based());
645
646 let date_edge2 = Field20::new("20991231".to_string());
647 assert!(date_edge2.is_date_based());
648
649 let date_edge3 = Field20::new("20240229".to_string()); assert!(date_edge3.is_date_based());
651 }
652
653 #[test]
654 fn test_field20_sequence_extraction_edge_cases() {
655 let zero_seq = Field20::new("FT000000000".to_string());
657 assert_eq!(zero_seq.sequence_number(), Some(0));
658
659 let single_digit = Field20::new("REF1".to_string());
661 assert_eq!(single_digit.sequence_number(), Some(1));
662
663 let max_seq = Field20::new("A1234567890".to_string());
665 assert_eq!(max_seq.sequence_number(), Some(1234567890));
666
667 let leading_zeros = Field20::new("TXN0000000001".to_string());
669 assert_eq!(leading_zeros.sequence_number(), Some(1));
670 }
671
672 #[test]
673 fn test_field20_validation_comprehensive() {
674 let valid_cases = vec![
676 "FT12345",
677 "TXN0000012345",
678 "20241201001",
679 "CUST123456789A",
680 "REF-123_456.78",
681 "ABC/DEF",
682 ];
683
684 for case in valid_cases {
685 let field = Field20::new(case.to_string());
686 let validation = field.validate();
687 assert!(validation.is_valid, "Failed validation for: {}", case);
688 }
689
690 let invalid_cases = vec![
692 "THIS_IS_WAY_TOO_LONG_FOR_FIELD20_VALIDATION", ];
694
695 for case in invalid_cases {
696 let field = Field20::new(case.to_string());
697 let validation = field.validate();
698 assert!(
699 !validation.is_valid,
700 "Should have failed validation for: {}",
701 case
702 );
703 }
704
705 let empty_field = Field20::new("".to_string());
707 let _validation = empty_field.validate();
708 assert!(!empty_field.is_well_formed());
710 }
711}