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 formatted.replace('.', ",")
420}
421
422pub fn format_swift_amount_for_currency(amount: f64, currency: &str) -> String {
443 let decimals = get_currency_decimals(currency);
444 format_swift_amount(amount, decimals as usize)
445}
446
447pub fn parse_date_yymmdd(input: &str) -> Result<NaiveDate, ParseError> {
449 if input.len() != 6 {
450 return Err(ParseError::InvalidFormat {
451 message: format!(
452 "Date must be in YYMMDD format (6 digits), found {} characters",
453 input.len()
454 ),
455 });
456 }
457
458 let year = input[0..2]
459 .parse::<u32>()
460 .map_err(|_| ParseError::InvalidFormat {
461 message: "Invalid year in date".to_string(),
462 })?;
463 let month = input[2..4]
464 .parse::<u32>()
465 .map_err(|_| ParseError::InvalidFormat {
466 message: "Invalid month in date".to_string(),
467 })?;
468 let day = input[4..6]
469 .parse::<u32>()
470 .map_err(|_| ParseError::InvalidFormat {
471 message: "Invalid day in date".to_string(),
472 })?;
473
474 let full_year = if year <= 49 { 2000 + year } else { 1900 + year };
476
477 NaiveDate::from_ymd_opt(full_year as i32, month, day).ok_or_else(|| ParseError::InvalidFormat {
478 message: format!("Invalid date: {}/{}/{}", full_year, month, day),
479 })
480}
481
482pub fn parse_date_yyyymmdd(input: &str) -> Result<NaiveDate, ParseError> {
484 if input.len() != 8 {
485 return Err(ParseError::InvalidFormat {
486 message: format!(
487 "Date must be in YYYYMMDD format (8 digits), found {} characters",
488 input.len()
489 ),
490 });
491 }
492
493 let year = input[0..4]
494 .parse::<i32>()
495 .map_err(|_| ParseError::InvalidFormat {
496 message: "Invalid year in date".to_string(),
497 })?;
498 let month = input[4..6]
499 .parse::<u32>()
500 .map_err(|_| ParseError::InvalidFormat {
501 message: "Invalid month in date".to_string(),
502 })?;
503 let day = input[6..8]
504 .parse::<u32>()
505 .map_err(|_| ParseError::InvalidFormat {
506 message: "Invalid day in date".to_string(),
507 })?;
508
509 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| ParseError::InvalidFormat {
510 message: format!("Invalid date: {}/{}/{}", year, month, day),
511 })
512}
513
514pub fn parse_time_hhmm(input: &str) -> Result<NaiveTime, ParseError> {
516 if input.len() != 4 {
517 return Err(ParseError::InvalidFormat {
518 message: format!(
519 "Time must be in HHMM format (4 digits), found {} characters",
520 input.len()
521 ),
522 });
523 }
524
525 let hour = input[0..2]
526 .parse::<u32>()
527 .map_err(|_| ParseError::InvalidFormat {
528 message: "Invalid hour in time".to_string(),
529 })?;
530 let minute = input[2..4]
531 .parse::<u32>()
532 .map_err(|_| ParseError::InvalidFormat {
533 message: "Invalid minute in time".to_string(),
534 })?;
535
536 NaiveTime::from_hms_opt(hour, minute, 0).ok_or_else(|| ParseError::InvalidFormat {
537 message: format!("Invalid time: {}:{}", hour, minute),
538 })
539}
540
541pub fn parse_datetime_yymmddhhmm(input: &str) -> Result<NaiveDateTime, ParseError> {
543 if input.len() != 10 {
544 return Err(ParseError::InvalidFormat {
545 message: format!(
546 "DateTime must be in YYMMDDHHMM format (10 digits), found {} characters",
547 input.len()
548 ),
549 });
550 }
551
552 let date = parse_date_yymmdd(&input[0..6])?;
553 let time = parse_time_hhmm(&input[6..10])?;
554
555 Ok(NaiveDateTime::new(date, time))
556}
557
558pub fn parse_reference(input: &str) -> Result<String, ParseError> {
560 parse_max_length(input, 16, "Reference")?;
561 parse_swift_chars(input, "Reference")?;
562 Ok(input.to_string())
563}
564
565pub fn split_at_first(input: &str, delimiter: char) -> (String, Option<String>) {
567 if let Some(pos) = input.find(delimiter) {
568 let (first, rest) = input.split_at(pos);
569 let rest = &rest[1..]; (
571 first.to_string(),
572 if rest.is_empty() {
573 None
574 } else {
575 Some(rest.to_string())
576 },
577 )
578 } else {
579 (input.to_string(), None)
580 }
581}
582
583pub fn split_at_newline(input: &str) -> (String, Option<String>) {
585 if let Some(pos) = input.find('\n') {
586 let (first, rest) = input.split_at(pos);
587 let rest = &rest[1..]; (
589 first.to_string(),
590 if rest.is_empty() {
591 None
592 } else {
593 Some(rest.to_string())
594 },
595 )
596 } else {
597 (input.to_string(), None)
598 }
599}
600
601pub fn normalize_text(input: &str) -> String {
603 input
604 .lines()
605 .map(|line| line.trim())
606 .filter(|line| !line.is_empty())
607 .collect::<Vec<_>>()
608 .join("\n")
609}
610
611pub fn validate_iban(iban: &str) -> Result<(), ParseError> {
613 if iban.len() < 15 || iban.len() > 34 {
615 return Err(ParseError::InvalidFormat {
616 message: format!(
617 "IBAN must be between 15 and 34 characters, found {}",
618 iban.len()
619 ),
620 });
621 }
622
623 if !iban[0..2].chars().all(|c| c.is_uppercase()) {
625 return Err(ParseError::InvalidFormat {
626 message: "IBAN country code must be uppercase letters".to_string(),
627 });
628 }
629
630 if !iban[2..4].chars().all(|c| c.is_numeric()) {
632 return Err(ParseError::InvalidFormat {
633 message: "IBAN check digits must be numeric".to_string(),
634 });
635 }
636
637 Ok(())
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use chrono::Datelike;
644
645 #[test]
646 fn test_parse_exact_length() {
647 assert!(parse_exact_length("ABC", 3, "Test").is_ok());
648 assert!(parse_exact_length("AB", 3, "Test").is_err());
649 assert!(parse_exact_length("ABCD", 3, "Test").is_err());
650 }
651
652 #[test]
653 fn test_parse_bic() {
654 assert!(parse_bic("DEUTDEFF").is_ok());
655 assert!(parse_bic("DEUTDEFFXXX").is_ok());
656 assert!(parse_bic("DEUT").is_err()); assert!(parse_bic("DEUTDEFFXX").is_err()); assert!(parse_bic("1234DEFF").is_err()); }
660
661 #[test]
662 fn test_parse_currency() {
663 assert!(parse_currency("USD").is_ok());
664 assert!(parse_currency("EUR").is_ok());
665 assert!(parse_currency("US").is_err()); assert!(parse_currency("usd").is_err()); }
668
669 #[test]
670 fn test_parse_date_yymmdd() {
671 let date = parse_date_yymmdd("231225").unwrap();
672 assert_eq!(date.year(), 2023);
673 assert_eq!(date.month(), 12);
674 assert_eq!(date.day(), 25);
675
676 let date = parse_date_yymmdd("991231").unwrap();
677 assert_eq!(date.year(), 1999);
678 }
679
680 #[test]
681 fn test_parse_amount() {
682 assert!(parse_amount("1234.56").is_ok());
683 assert!(parse_amount("1234,56").is_ok()); assert!(parse_amount("1234").is_ok());
685 assert!(parse_amount("abc").is_err());
686 }
687
688 #[test]
689 fn test_format_swift_amount() {
690 assert_eq!(format_swift_amount(1234.56, 2), "1234,56");
692 assert_eq!(format_swift_amount(1000.00, 2), "1000,00");
693 assert_eq!(format_swift_amount(1000.50, 2), "1000,50");
694
695 assert_eq!(format_swift_amount(5000.00, 2), "5000,00");
697 assert_eq!(format_swift_amount(2500.00, 2), "2500,00");
698
699 assert_eq!(format_swift_amount(250.75, 2), "250,75");
701 assert_eq!(format_swift_amount(99.99, 2), "99,99");
702
703 assert_eq!(format_swift_amount(1000000.0, 2), "1000000,00");
705 assert_eq!(format_swift_amount(1234567.89, 2), "1234567,89");
706
707 assert_eq!(format_swift_amount(1500000.0, 0), "1500000");
709
710 assert_eq!(format_swift_amount(123.456, 3), "123,456");
712 }
713
714 #[test]
715 fn test_split_at_first() {
716 let (first, rest) = split_at_first("ABC/DEF/GHI", '/');
717 assert_eq!(first, "ABC");
718 assert_eq!(rest, Some("DEF/GHI".to_string()));
719
720 let (first, rest) = split_at_first("ABCDEF", '/');
721 assert_eq!(first, "ABCDEF");
722 assert_eq!(rest, None);
723 }
724
725 #[test]
726 fn test_validate_iban() {
727 assert!(validate_iban("DE89370400440532013000").is_ok());
728 assert!(validate_iban("GB82WEST12345698765432").is_ok());
729 assert!(validate_iban("DE89").is_err()); assert!(validate_iban("1234567890123456").is_err()); }
732
733 #[test]
734 fn test_get_currency_decimals() {
735 assert_eq!(get_currency_decimals("JPY"), 0);
737 assert_eq!(get_currency_decimals("KRW"), 0);
738 assert_eq!(get_currency_decimals("VND"), 0);
739 assert_eq!(get_currency_decimals("CLP"), 0);
740
741 assert_eq!(get_currency_decimals("USD"), 2);
743 assert_eq!(get_currency_decimals("EUR"), 2);
744 assert_eq!(get_currency_decimals("GBP"), 2);
745 assert_eq!(get_currency_decimals("CHF"), 2);
746
747 assert_eq!(get_currency_decimals("BHD"), 3);
749 assert_eq!(get_currency_decimals("KWD"), 3);
750 assert_eq!(get_currency_decimals("OMR"), 3);
751 assert_eq!(get_currency_decimals("TND"), 3);
752
753 assert_eq!(get_currency_decimals("CLF"), 4);
755
756 assert_eq!(get_currency_decimals("XXX"), 2);
758 }
759
760 #[test]
761 fn test_validate_non_commodity_currency() {
762 assert!(validate_non_commodity_currency("USD").is_ok());
764 assert!(validate_non_commodity_currency("EUR").is_ok());
765 assert!(validate_non_commodity_currency("JPY").is_ok());
766
767 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();
775 if let ParseError::InvalidFormat { message } = err {
776 assert!(message.contains("C08"));
777 assert!(message.contains("XAU"));
778 } else {
779 panic!("Expected InvalidFormat error");
780 }
781 }
782
783 #[test]
784 fn test_parse_currency_non_commodity() {
785 assert_eq!(parse_currency_non_commodity("USD").unwrap(), "USD");
787 assert_eq!(parse_currency_non_commodity("EUR").unwrap(), "EUR");
788 assert_eq!(parse_currency_non_commodity("JPY").unwrap(), "JPY");
789
790 assert!(parse_currency_non_commodity("XAU").is_err());
792 assert!(parse_currency_non_commodity("XAG").is_err());
793
794 assert!(parse_currency_non_commodity("US").is_err()); assert!(parse_currency_non_commodity("usd").is_err()); }
798
799 #[test]
800 fn test_validate_amount_decimals() {
801 assert!(validate_amount_decimals(100.0, "USD").is_ok());
803 assert!(validate_amount_decimals(100.5, "USD").is_ok());
804 assert!(validate_amount_decimals(100.50, "USD").is_ok());
805 assert!(validate_amount_decimals(100.505, "USD").is_err()); assert!(validate_amount_decimals(100.0, "JPY").is_ok());
809 assert!(validate_amount_decimals(1500000.0, "JPY").is_ok());
810 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());
815 assert!(validate_amount_decimals(100.5, "BHD").is_ok());
816 assert!(validate_amount_decimals(100.505, "BHD").is_ok());
817 assert!(validate_amount_decimals(100.5055, "BHD").is_err()); let err = validate_amount_decimals(100.505, "USD").unwrap_err();
821 if let ParseError::InvalidFormat { message } = err {
822 assert!(message.contains("C03"));
823 assert!(message.contains("USD"));
824 } else {
825 panic!("Expected InvalidFormat error");
826 }
827 }
828
829 #[test]
830 fn test_parse_amount_with_currency() {
831 assert_eq!(parse_amount_with_currency("100.50", "USD").unwrap(), 100.50);
833 assert_eq!(
834 parse_amount_with_currency("1500000", "JPY").unwrap(),
835 1500000.0
836 );
837 assert_eq!(
838 parse_amount_with_currency("100.505", "BHD").unwrap(),
839 100.505
840 );
841
842 assert_eq!(parse_amount_with_currency("100,50", "EUR").unwrap(), 100.50);
844
845 assert!(parse_amount_with_currency("100.505", "USD").is_err());
847 assert!(parse_amount_with_currency("100.5", "JPY").is_err());
848 assert!(parse_amount_with_currency("100.5055", "BHD").is_err());
849
850 assert!(parse_amount_with_currency("abc", "USD").is_err());
852 }
853
854 #[test]
855 fn test_format_swift_amount_for_currency() {
856 assert_eq!(format_swift_amount_for_currency(1234.56, "USD"), "1234,56");
858 assert_eq!(format_swift_amount_for_currency(1000.00, "USD"), "1000,00");
859 assert_eq!(format_swift_amount_for_currency(1000.50, "USD"), "1000,50");
860
861 assert_eq!(
863 format_swift_amount_for_currency(1500000.0, "JPY"),
864 "1500000"
865 );
866 assert_eq!(format_swift_amount_for_currency(1234.0, "JPY"), "1234");
867
868 assert_eq!(format_swift_amount_for_currency(123.456, "BHD"), "123,456");
870 assert_eq!(format_swift_amount_for_currency(100.5, "BHD"), "100,500");
871 assert_eq!(format_swift_amount_for_currency(100.0, "BHD"), "100,000");
872
873 assert_eq!(format_swift_amount_for_currency(5000.00, "EUR"), "5000,00");
875 assert_eq!(format_swift_amount_for_currency(250.75, "EUR"), "250,75");
876 }
877}