1use crate::errors::ParseError;
8use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
9
10pub fn parse_exact_length(
12 input: &str,
13 expected_len: usize,
14 field_name: &str,
15) -> Result<String, ParseError> {
16 if input.len() != expected_len {
17 return Err(ParseError::InvalidFormat {
18 message: format!(
19 "{} must be exactly {} characters, found {}",
20 field_name,
21 expected_len,
22 input.len()
23 ),
24 });
25 }
26 Ok(input.to_string())
27}
28
29pub fn parse_max_length(
31 input: &str,
32 max_len: usize,
33 field_name: &str,
34) -> Result<String, ParseError> {
35 if input.len() > max_len {
36 return Err(ParseError::InvalidFormat {
37 message: format!(
38 "{} must be at most {} characters, found {}",
39 field_name,
40 max_len,
41 input.len()
42 ),
43 });
44 }
45 Ok(input.to_string())
46}
47
48pub fn parse_length_range(
50 input: &str,
51 min_len: usize,
52 max_len: usize,
53 field_name: &str,
54) -> Result<String, ParseError> {
55 if input.len() < min_len || input.len() > max_len {
56 return Err(ParseError::InvalidFormat {
57 message: format!(
58 "{} must be between {} and {} characters, found {}",
59 field_name,
60 min_len,
61 max_len,
62 input.len()
63 ),
64 });
65 }
66 Ok(input.to_string())
67}
68
69pub fn parse_alphanumeric(input: &str, field_name: &str) -> Result<String, ParseError> {
71 if !input.chars().all(|c| c.is_alphanumeric()) {
72 return Err(ParseError::InvalidFormat {
73 message: format!("{} must contain only letters and digits", field_name),
74 });
75 }
76 Ok(input.to_string())
77}
78
79pub fn parse_uppercase(input: &str, field_name: &str) -> Result<String, ParseError> {
81 if !input.chars().all(|c| c.is_uppercase() || c.is_whitespace()) {
82 return Err(ParseError::InvalidFormat {
83 message: format!("{} must contain only uppercase letters", field_name),
84 });
85 }
86 Ok(input.to_string())
87}
88
89pub fn parse_numeric(input: &str, field_name: &str) -> Result<String, ParseError> {
91 if !input.chars().all(|c| c.is_numeric()) {
92 return Err(ParseError::InvalidFormat {
93 message: format!("{} must contain only digits", field_name),
94 });
95 }
96 Ok(input.to_string())
97}
98
99pub fn parse_swift_digits(input: &str, field_name: &str) -> Result<String, ParseError> {
101 if !input.chars().all(|c| c.is_ascii_digit()) {
102 return Err(ParseError::InvalidFormat {
103 message: format!("{} must contain only digits", field_name),
104 });
105 }
106 Ok(input.to_string())
107}
108
109pub fn parse_swift_chars(input: &str, field_name: &str) -> Result<String, ParseError> {
114 const SWIFT_SPECIAL: &str = "/-?:().,'+{} \r\n%&*;<=>@[]_$!\"#|";
117
118 if !input
119 .chars()
120 .all(|c| c.is_alphanumeric() || SWIFT_SPECIAL.contains(c))
121 {
122 return Err(ParseError::InvalidFormat {
123 message: format!(
124 "{} contains invalid characters for SWIFT format",
125 field_name
126 ),
127 });
128 }
129 Ok(input.to_string())
130}
131
132pub fn parse_bic(input: &str) -> Result<String, ParseError> {
134 if input.len() != 8 && input.len() != 11 {
135 return Err(ParseError::InvalidFormat {
136 message: format!("BIC must be 8 or 11 characters, found {}", input.len()),
137 });
138 }
139
140 if !input[0..4].chars().all(|c| c.is_alphabetic()) {
142 return Err(ParseError::InvalidFormat {
143 message: "BIC bank code (first 4 chars) must be letters".to_string(),
144 });
145 }
146
147 if !input[4..6].chars().all(|c| c.is_alphabetic()) {
149 return Err(ParseError::InvalidFormat {
150 message: "BIC country code (chars 5-6) must be letters".to_string(),
151 });
152 }
153
154 if !input[6..8].chars().all(|c| c.is_alphanumeric()) {
156 return Err(ParseError::InvalidFormat {
157 message: "BIC location code (chars 7-8) must be alphanumeric".to_string(),
158 });
159 }
160
161 if input.len() == 11 && !input[8..11].chars().all(|c| c.is_alphanumeric()) {
163 return Err(ParseError::InvalidFormat {
164 message: "BIC branch code (chars 9-11) must be alphanumeric".to_string(),
165 });
166 }
167
168 Ok(input.to_string())
169}
170
171pub fn parse_account(input: &str) -> Result<String, ParseError> {
173 parse_max_length(input, 34, "Account")?;
174 parse_swift_chars(input, "Account")?;
175 Ok(input.to_string())
176}
177
178pub fn get_currency_decimals(currency: &str) -> u8 {
194 match currency {
195 "BIF" | "CLP" | "DJF" | "GNF" | "ISK" | "JPY" | "KMF" | "KRW" | "PYG" | "RWF" | "UGX" | "UYI" | "VND" | "VUV" | "XAF" | "XOF" | "XPF" => 0,
214
215 "BHD" | "IQD" | "JOD" | "KWD" | "LYD" | "OMR" | "TND" => 3,
224
225 "CLF" | "UYW" => 4,
229
230 _ => 2,
232 }
233}
234
235const COMMODITY_CURRENCIES: &[&str] = &[
237 "XAU", "XAG", "XPD", "XPT", ];
242
243pub fn validate_non_commodity_currency(currency: &str) -> Result<(), ParseError> {
257 if COMMODITY_CURRENCIES.contains(¤cy) {
258 return Err(ParseError::InvalidFormat {
259 message: format!(
260 "Commodity currency code {} not allowed in payment messages (Error code: C08)",
261 currency
262 ),
263 });
264 }
265 Ok(())
266}
267
268pub fn parse_currency(input: &str) -> Result<String, ParseError> {
270 if input.len() != 3 {
271 return Err(ParseError::InvalidFormat {
272 message: format!(
273 "Currency code must be exactly 3 characters, found {}",
274 input.len()
275 ),
276 });
277 }
278
279 if !input.chars().all(|c| c.is_uppercase()) {
280 return Err(ParseError::InvalidFormat {
281 message: "Currency code must be uppercase letters".to_string(),
282 });
283 }
284
285 Ok(input.to_string())
286}
287
288pub fn parse_currency_non_commodity(input: &str) -> Result<String, ParseError> {
305 let currency = parse_currency(input)?;
306 validate_non_commodity_currency(¤cy)?;
307 Ok(currency)
308}
309
310pub fn parse_amount(input: &str) -> Result<f64, ParseError> {
312 let normalized = input.replace(',', ".");
314
315 normalized
316 .parse::<f64>()
317 .map_err(|e| ParseError::InvalidFormat {
318 message: format!("Invalid amount format: {}", e),
319 })
320}
321
322pub fn validate_amount_decimals(amount: f64, currency: &str) -> Result<(), ParseError> {
347 let max_decimals = get_currency_decimals(currency);
348
349 let amount_str = format!("{:.10}", amount); let decimal_places = if let Some(dot_pos) = amount_str.find('.') {
353 let after_dot = &amount_str[dot_pos + 1..];
354 after_dot.trim_end_matches('0').len()
356 } else {
357 0
358 };
359
360 if decimal_places > max_decimals as usize {
361 return Err(ParseError::InvalidFormat {
362 message: format!(
363 "Amount has {} decimal places but currency {} allows maximum {} (Error code: C03)",
364 decimal_places, currency, max_decimals
365 ),
366 });
367 }
368
369 Ok(())
370}
371
372pub fn parse_amount_with_currency(input: &str, currency: &str) -> Result<f64, ParseError> {
389 let amount = parse_amount(input)?;
390 validate_amount_decimals(amount, currency)?;
391 Ok(amount)
392}
393
394pub fn format_swift_amount(amount: f64, decimals: usize) -> String {
418 let formatted = format!("{:.width$}", amount, width = decimals);
419 let with_comma = formatted.replace('.', ",");
420
421 if with_comma.contains(',') {
424 let trimmed = with_comma.trim_end_matches('0');
425 if trimmed.ends_with(',') {
426 trimmed.trim_end_matches(',').to_string()
427 } else {
428 trimmed.to_string()
429 }
430 } else {
431 with_comma
432 }
433}
434
435pub fn format_swift_amount_for_currency(amount: f64, currency: &str) -> String {
456 let decimals = get_currency_decimals(currency);
457 format_swift_amount(amount, decimals as usize)
458}
459
460pub fn parse_date_yymmdd(input: &str) -> Result<NaiveDate, ParseError> {
462 if input.len() != 6 {
463 return Err(ParseError::InvalidFormat {
464 message: format!(
465 "Date must be in YYMMDD format (6 digits), found {} characters",
466 input.len()
467 ),
468 });
469 }
470
471 let year = input[0..2]
472 .parse::<u32>()
473 .map_err(|_| ParseError::InvalidFormat {
474 message: "Invalid year in date".to_string(),
475 })?;
476 let month = input[2..4]
477 .parse::<u32>()
478 .map_err(|_| ParseError::InvalidFormat {
479 message: "Invalid month in date".to_string(),
480 })?;
481 let day = input[4..6]
482 .parse::<u32>()
483 .map_err(|_| ParseError::InvalidFormat {
484 message: "Invalid day in date".to_string(),
485 })?;
486
487 let full_year = if year <= 49 { 2000 + year } else { 1900 + year };
489
490 NaiveDate::from_ymd_opt(full_year as i32, month, day).ok_or_else(|| ParseError::InvalidFormat {
491 message: format!("Invalid date: {}/{}/{}", full_year, month, day),
492 })
493}
494
495pub fn parse_date_yyyymmdd(input: &str) -> Result<NaiveDate, ParseError> {
497 if input.len() != 8 {
498 return Err(ParseError::InvalidFormat {
499 message: format!(
500 "Date must be in YYYYMMDD format (8 digits), found {} characters",
501 input.len()
502 ),
503 });
504 }
505
506 let year = input[0..4]
507 .parse::<i32>()
508 .map_err(|_| ParseError::InvalidFormat {
509 message: "Invalid year in date".to_string(),
510 })?;
511 let month = input[4..6]
512 .parse::<u32>()
513 .map_err(|_| ParseError::InvalidFormat {
514 message: "Invalid month in date".to_string(),
515 })?;
516 let day = input[6..8]
517 .parse::<u32>()
518 .map_err(|_| ParseError::InvalidFormat {
519 message: "Invalid day in date".to_string(),
520 })?;
521
522 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| ParseError::InvalidFormat {
523 message: format!("Invalid date: {}/{}/{}", year, month, day),
524 })
525}
526
527pub fn parse_time_hhmm(input: &str) -> Result<NaiveTime, ParseError> {
529 if input.len() != 4 {
530 return Err(ParseError::InvalidFormat {
531 message: format!(
532 "Time must be in HHMM format (4 digits), found {} characters",
533 input.len()
534 ),
535 });
536 }
537
538 let hour = input[0..2]
539 .parse::<u32>()
540 .map_err(|_| ParseError::InvalidFormat {
541 message: "Invalid hour in time".to_string(),
542 })?;
543 let minute = input[2..4]
544 .parse::<u32>()
545 .map_err(|_| ParseError::InvalidFormat {
546 message: "Invalid minute in time".to_string(),
547 })?;
548
549 NaiveTime::from_hms_opt(hour, minute, 0).ok_or_else(|| ParseError::InvalidFormat {
550 message: format!("Invalid time: {}:{}", hour, minute),
551 })
552}
553
554pub fn parse_datetime_yymmddhhmm(input: &str) -> Result<NaiveDateTime, ParseError> {
556 if input.len() != 10 {
557 return Err(ParseError::InvalidFormat {
558 message: format!(
559 "DateTime must be in YYMMDDHHMM format (10 digits), found {} characters",
560 input.len()
561 ),
562 });
563 }
564
565 let date = parse_date_yymmdd(&input[0..6])?;
566 let time = parse_time_hhmm(&input[6..10])?;
567
568 Ok(NaiveDateTime::new(date, time))
569}
570
571pub fn parse_reference(input: &str) -> Result<String, ParseError> {
573 parse_max_length(input, 16, "Reference")?;
574 parse_swift_chars(input, "Reference")?;
575 Ok(input.to_string())
576}
577
578pub fn split_at_first(input: &str, delimiter: char) -> (String, Option<String>) {
580 if let Some(pos) = input.find(delimiter) {
581 let (first, rest) = input.split_at(pos);
582 let rest = &rest[1..]; (
584 first.to_string(),
585 if rest.is_empty() {
586 None
587 } else {
588 Some(rest.to_string())
589 },
590 )
591 } else {
592 (input.to_string(), None)
593 }
594}
595
596pub fn split_at_newline(input: &str) -> (String, Option<String>) {
598 if let Some(pos) = input.find('\n') {
599 let (first, rest) = input.split_at(pos);
600 let rest = &rest[1..]; (
602 first.to_string(),
603 if rest.is_empty() {
604 None
605 } else {
606 Some(rest.to_string())
607 },
608 )
609 } else {
610 (input.to_string(), None)
611 }
612}
613
614pub fn normalize_text(input: &str) -> String {
616 input
617 .lines()
618 .map(|line| line.trim())
619 .filter(|line| !line.is_empty())
620 .collect::<Vec<_>>()
621 .join("\n")
622}
623
624pub fn validate_iban(iban: &str) -> Result<(), ParseError> {
626 if iban.len() < 15 || iban.len() > 34 {
628 return Err(ParseError::InvalidFormat {
629 message: format!(
630 "IBAN must be between 15 and 34 characters, found {}",
631 iban.len()
632 ),
633 });
634 }
635
636 if !iban[0..2].chars().all(|c| c.is_uppercase()) {
638 return Err(ParseError::InvalidFormat {
639 message: "IBAN country code must be uppercase letters".to_string(),
640 });
641 }
642
643 if !iban[2..4].chars().all(|c| c.is_numeric()) {
645 return Err(ParseError::InvalidFormat {
646 message: "IBAN check digits must be numeric".to_string(),
647 });
648 }
649
650 Ok(())
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use chrono::Datelike;
657
658 #[test]
659 fn test_parse_exact_length() {
660 assert!(parse_exact_length("ABC", 3, "Test").is_ok());
661 assert!(parse_exact_length("AB", 3, "Test").is_err());
662 assert!(parse_exact_length("ABCD", 3, "Test").is_err());
663 }
664
665 #[test]
666 fn test_parse_bic() {
667 assert!(parse_bic("DEUTDEFF").is_ok());
668 assert!(parse_bic("DEUTDEFFXXX").is_ok());
669 assert!(parse_bic("DEUT").is_err()); assert!(parse_bic("DEUTDEFFXX").is_err()); assert!(parse_bic("1234DEFF").is_err()); }
673
674 #[test]
675 fn test_parse_currency() {
676 assert!(parse_currency("USD").is_ok());
677 assert!(parse_currency("EUR").is_ok());
678 assert!(parse_currency("US").is_err()); assert!(parse_currency("usd").is_err()); }
681
682 #[test]
683 fn test_parse_date_yymmdd() {
684 let date = parse_date_yymmdd("231225").unwrap();
685 assert_eq!(date.year(), 2023);
686 assert_eq!(date.month(), 12);
687 assert_eq!(date.day(), 25);
688
689 let date = parse_date_yymmdd("991231").unwrap();
690 assert_eq!(date.year(), 1999);
691 }
692
693 #[test]
694 fn test_parse_amount() {
695 assert!(parse_amount("1234.56").is_ok());
696 assert!(parse_amount("1234,56").is_ok()); assert!(parse_amount("1234").is_ok());
698 assert!(parse_amount("abc").is_err());
699 }
700
701 #[test]
702 fn test_format_swift_amount() {
703 assert_eq!(format_swift_amount(1234.56, 2), "1234,56");
705 assert_eq!(format_swift_amount(1000.00, 2), "1000");
706 assert_eq!(format_swift_amount(1000.50, 2), "1000,5");
707
708 assert_eq!(format_swift_amount(5000.00, 2), "5000");
710 assert_eq!(format_swift_amount(2500.00, 2), "2500");
711
712 assert_eq!(format_swift_amount(250.75, 2), "250,75");
714 assert_eq!(format_swift_amount(99.99, 2), "99,99");
715
716 assert_eq!(format_swift_amount(1000000.0, 2), "1000000");
718 assert_eq!(format_swift_amount(1234567.89, 2), "1234567,89");
719
720 assert_eq!(format_swift_amount(1500000.0, 0), "1500000");
722
723 assert_eq!(format_swift_amount(123.456, 3), "123,456");
725 }
726
727 #[test]
728 fn test_split_at_first() {
729 let (first, rest) = split_at_first("ABC/DEF/GHI", '/');
730 assert_eq!(first, "ABC");
731 assert_eq!(rest, Some("DEF/GHI".to_string()));
732
733 let (first, rest) = split_at_first("ABCDEF", '/');
734 assert_eq!(first, "ABCDEF");
735 assert_eq!(rest, None);
736 }
737
738 #[test]
739 fn test_validate_iban() {
740 assert!(validate_iban("DE89370400440532013000").is_ok());
741 assert!(validate_iban("GB82WEST12345698765432").is_ok());
742 assert!(validate_iban("DE89").is_err()); assert!(validate_iban("1234567890123456").is_err()); }
745
746 #[test]
747 fn test_get_currency_decimals() {
748 assert_eq!(get_currency_decimals("JPY"), 0);
750 assert_eq!(get_currency_decimals("KRW"), 0);
751 assert_eq!(get_currency_decimals("VND"), 0);
752 assert_eq!(get_currency_decimals("CLP"), 0);
753
754 assert_eq!(get_currency_decimals("USD"), 2);
756 assert_eq!(get_currency_decimals("EUR"), 2);
757 assert_eq!(get_currency_decimals("GBP"), 2);
758 assert_eq!(get_currency_decimals("CHF"), 2);
759
760 assert_eq!(get_currency_decimals("BHD"), 3);
762 assert_eq!(get_currency_decimals("KWD"), 3);
763 assert_eq!(get_currency_decimals("OMR"), 3);
764 assert_eq!(get_currency_decimals("TND"), 3);
765
766 assert_eq!(get_currency_decimals("CLF"), 4);
768
769 assert_eq!(get_currency_decimals("XXX"), 2);
771 }
772
773 #[test]
774 fn test_validate_non_commodity_currency() {
775 assert!(validate_non_commodity_currency("USD").is_ok());
777 assert!(validate_non_commodity_currency("EUR").is_ok());
778 assert!(validate_non_commodity_currency("JPY").is_ok());
779
780 assert!(validate_non_commodity_currency("XAU").is_err()); assert!(validate_non_commodity_currency("XAG").is_err()); assert!(validate_non_commodity_currency("XPD").is_err()); assert!(validate_non_commodity_currency("XPT").is_err()); let err = validate_non_commodity_currency("XAU").unwrap_err();
788 if let ParseError::InvalidFormat { message } = err {
789 assert!(message.contains("C08"));
790 assert!(message.contains("XAU"));
791 } else {
792 panic!("Expected InvalidFormat error");
793 }
794 }
795
796 #[test]
797 fn test_parse_currency_non_commodity() {
798 assert_eq!(parse_currency_non_commodity("USD").unwrap(), "USD");
800 assert_eq!(parse_currency_non_commodity("EUR").unwrap(), "EUR");
801 assert_eq!(parse_currency_non_commodity("JPY").unwrap(), "JPY");
802
803 assert!(parse_currency_non_commodity("XAU").is_err());
805 assert!(parse_currency_non_commodity("XAG").is_err());
806
807 assert!(parse_currency_non_commodity("US").is_err()); assert!(parse_currency_non_commodity("usd").is_err()); }
811
812 #[test]
813 fn test_validate_amount_decimals() {
814 assert!(validate_amount_decimals(100.0, "USD").is_ok());
816 assert!(validate_amount_decimals(100.5, "USD").is_ok());
817 assert!(validate_amount_decimals(100.50, "USD").is_ok());
818 assert!(validate_amount_decimals(100.505, "USD").is_err()); assert!(validate_amount_decimals(100.0, "JPY").is_ok());
822 assert!(validate_amount_decimals(1500000.0, "JPY").is_ok());
823 assert!(validate_amount_decimals(100.5, "JPY").is_err()); assert!(validate_amount_decimals(100.50, "JPY").is_err()); assert!(validate_amount_decimals(100.0, "BHD").is_ok());
828 assert!(validate_amount_decimals(100.5, "BHD").is_ok());
829 assert!(validate_amount_decimals(100.505, "BHD").is_ok());
830 assert!(validate_amount_decimals(100.5055, "BHD").is_err()); let err = validate_amount_decimals(100.505, "USD").unwrap_err();
834 if let ParseError::InvalidFormat { message } = err {
835 assert!(message.contains("C03"));
836 assert!(message.contains("USD"));
837 } else {
838 panic!("Expected InvalidFormat error");
839 }
840 }
841
842 #[test]
843 fn test_parse_amount_with_currency() {
844 assert_eq!(parse_amount_with_currency("100.50", "USD").unwrap(), 100.50);
846 assert_eq!(
847 parse_amount_with_currency("1500000", "JPY").unwrap(),
848 1500000.0
849 );
850 assert_eq!(
851 parse_amount_with_currency("100.505", "BHD").unwrap(),
852 100.505
853 );
854
855 assert_eq!(parse_amount_with_currency("100,50", "EUR").unwrap(), 100.50);
857
858 assert!(parse_amount_with_currency("100.505", "USD").is_err());
860 assert!(parse_amount_with_currency("100.5", "JPY").is_err());
861 assert!(parse_amount_with_currency("100.5055", "BHD").is_err());
862
863 assert!(parse_amount_with_currency("abc", "USD").is_err());
865 }
866
867 #[test]
868 fn test_format_swift_amount_for_currency() {
869 assert_eq!(format_swift_amount_for_currency(1234.56, "USD"), "1234,56");
871 assert_eq!(format_swift_amount_for_currency(1000.00, "USD"), "1000");
872 assert_eq!(format_swift_amount_for_currency(1000.50, "USD"), "1000,5");
873
874 assert_eq!(
876 format_swift_amount_for_currency(1500000.0, "JPY"),
877 "1500000"
878 );
879 assert_eq!(format_swift_amount_for_currency(1234.0, "JPY"), "1234");
880
881 assert_eq!(format_swift_amount_for_currency(123.456, "BHD"), "123,456");
883 assert_eq!(format_swift_amount_for_currency(100.5, "BHD"), "100,5");
884 assert_eq!(format_swift_amount_for_currency(100.0, "BHD"), "100");
885
886 assert_eq!(format_swift_amount_for_currency(5000.00, "EUR"), "5000");
888 assert_eq!(format_swift_amount_for_currency(250.75, "EUR"), "250,75");
889 }
890}