swift_mt_message/fields/
swift_utils.rs

1//! # Core SWIFT Utility Functions
2//!
3//! Low-level parsing utilities for SWIFT MT message primitive data types.
4//! These utilities handle basic SWIFT data formats like BIC codes, currency codes,
5//! dates, amounts, and character validation.
6
7use crate::errors::ParseError;
8use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
9
10/// Parse a string with exact length requirement
11pub 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
29/// Parse a string with maximum length limit
30pub 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
48/// Parse a string with minimum and maximum length
49pub 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
69/// Parse alphanumeric string (letters and digits only)
70pub 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
79/// Parse uppercase letters only
80pub 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
89/// Parse numeric string (digits only)
90pub 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
99/// Parse SWIFT digits format (digits only, used for numeric fields)
100pub 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
109/// Parse SWIFT character set (a-z, A-Z, 0-9, and special chars)
110///
111/// SWIFT 'x' character set includes: a-z, A-Z, 0-9, and special characters:
112/// / - ? : ( ) . , ' + { } SPACE CR LF and other printable ASCII
113pub fn parse_swift_chars(input: &str, field_name: &str) -> Result<String, ParseError> {
114    // SWIFT x character set: alphanumeric + special characters
115    // Common special chars: / - ? : ( ) . , ' + { } SPACE CR LF % & * ; < = > @ [ ] _ $ ! " # |
116    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
132/// Parse BIC code (8 or 11 characters)
133pub 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    // First 4 chars: Bank code (letters)
141    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    // Next 2 chars: Country code (letters)
148    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    // Next 2 chars: Location code (alphanumeric)
155    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    // Optional 3 chars: Branch code (alphanumeric)
162    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
171/// Parse account number (max 34 characters)
172pub 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
178/// Get the number of decimal places for a currency according to ISO 4217
179///
180/// Returns the standard number of decimal places for the given currency code.
181/// Most currencies use 2 decimal places, but there are notable exceptions.
182///
183/// # Examples
184/// - JPY, KRW: 0 decimal places (yen, won are not subdivided)
185/// - BHD, KWD, OMR, TND: 3 decimal places (dinars subdivided into 1000 fils)
186/// - Most others: 2 decimal places (USD, EUR, GBP, etc.)
187///
188/// # Arguments
189/// * `currency` - ISO 4217 three-letter currency code
190///
191/// # Returns
192/// Number of decimal places (0, 2, or 3)
193pub fn get_currency_decimals(currency: &str) -> u8 {
194    match currency {
195        // Zero decimal currencies (not subdivided)
196        "BIF" | // Burundian Franc
197        "CLP" | // Chilean Peso
198        "DJF" | // Djiboutian Franc
199        "GNF" | // Guinean Franc
200        "ISK" | // Icelandic Króna
201        "JPY" | // Japanese Yen
202        "KMF" | // Comorian Franc
203        "KRW" | // South Korean Won
204        "PYG" | // Paraguayan Guaraní
205        "RWF" | // Rwandan Franc
206        "UGX" | // Ugandan Shilling
207        "UYI" | // Uruguay Peso en Unidades Indexadas
208        "VND" | // Vietnamese Đồng
209        "VUV" | // Vanuatu Vatu
210        "XAF" | // Central African CFA Franc
211        "XOF" | // West African CFA Franc
212        "XPF"   // CFP Franc
213        => 0,
214
215        // Three decimal currencies (subdivided into 1000)
216        "BHD" | // Bahraini Dinar
217        "IQD" | // Iraqi Dinar
218        "JOD" | // Jordanian Dinar
219        "KWD" | // Kuwaiti Dinar
220        "LYD" | // Libyan Dinar
221        "OMR" | // Omani Rial
222        "TND"   // Tunisian Dinar
223        => 3,
224
225        // Four decimal currencies (rare)
226        "CLF" | // Unidad de Fomento (Chile)
227        "UYW"   // Unidad Previsional (Uruguay)
228        => 4,
229
230        // Default: two decimal places (USD, EUR, GBP, CHF, CAD, AUD, etc.)
231        _ => 2,
232    }
233}
234
235/// Commodity currency codes that are not allowed in payment messages (C08 validation)
236const COMMODITY_CURRENCIES: &[&str] = &[
237    "XAU", // Gold
238    "XAG", // Silver
239    "XPD", // Palladium
240    "XPT", // Platinum
241];
242
243/// Validate that currency is not a commodity code (C08 validation)
244///
245/// SWIFT network validation rule C08 prohibits the use of commodity currency codes
246/// (XAU, XAG, XPD, XPT) in payment message amount fields.
247///
248/// # Arguments
249/// * `currency` - ISO 4217 currency code to validate
250///
251/// # Returns
252/// Ok(()) if valid, Err(ParseError) if commodity currency
253///
254/// # Errors
255/// Returns ParseError::InvalidFormat with C08 error if commodity currency detected
256pub fn validate_non_commodity_currency(currency: &str) -> Result<(), ParseError> {
257    if COMMODITY_CURRENCIES.contains(&currency) {
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
268/// Parse currency code (3 uppercase letters)
269pub 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
288/// Parse currency code with commodity validation (enforces C08 rule)
289///
290/// This is a stricter version of parse_currency that also validates against
291/// commodity currencies. Use this for amount fields (32A, 32B, 33B, 71F, 71G, etc.)
292///
293/// # Arguments
294/// * `input` - Currency code string to parse
295///
296/// # Returns
297/// Validated currency code string
298///
299/// # Errors
300/// Returns error if:
301/// - Not exactly 3 characters (T52)
302/// - Contains non-uppercase letters (T52)
303/// - Is a commodity currency code (C08)
304pub fn parse_currency_non_commodity(input: &str) -> Result<String, ParseError> {
305    let currency = parse_currency(input)?;
306    validate_non_commodity_currency(&currency)?;
307    Ok(currency)
308}
309
310/// Parse amount with optional decimal places
311pub fn parse_amount(input: &str) -> Result<f64, ParseError> {
312    // Remove any commas (European decimal separator handling)
313    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
322/// Validate amount decimal precision for a specific currency (C03 validation)
323///
324/// SWIFT network validation rule C03 requires that the number of decimal places
325/// in an amount must not exceed the maximum allowed for the currency.
326///
327/// # Arguments
328/// * `amount` - The amount value to validate
329/// * `currency` - ISO 4217 currency code
330///
331/// # Returns
332/// Ok(()) if decimal precision is valid, Err(ParseError) if exceeds limit
333///
334/// # Errors
335/// Returns ParseError::InvalidFormat with C03 error if decimal precision exceeded
336///
337/// # Examples
338/// ```
339/// use swift_mt_message::fields::swift_utils::validate_amount_decimals;
340///
341/// validate_amount_decimals(100.50, "USD").unwrap(); // Ok - 2 decimals allowed
342/// validate_amount_decimals(100.0, "JPY").unwrap();  // Ok - 0 decimals allowed
343/// assert!(validate_amount_decimals(100.50, "JPY").is_err()); // JPY allows 0 decimals only
344/// assert!(validate_amount_decimals(100.5055, "BHD").is_err()); // BHD allows 3 decimals max
345/// ```
346pub fn validate_amount_decimals(amount: f64, currency: &str) -> Result<(), ParseError> {
347    let max_decimals = get_currency_decimals(currency);
348
349    // Calculate actual decimal places in the amount
350    // Use string representation to avoid floating point precision issues
351    let amount_str = format!("{:.10}", amount); // Format with high precision
352    let decimal_places = if let Some(dot_pos) = amount_str.find('.') {
353        let after_dot = &amount_str[dot_pos + 1..];
354        // Count non-zero digits after decimal point
355        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
372/// Parse amount with currency-specific decimal validation
373///
374/// This combines amount parsing with currency-specific decimal precision validation.
375/// Use this for amount fields where the currency is known (Field 32A, 32B, etc.)
376///
377/// # Arguments
378/// * `input` - Amount string to parse
379/// * `currency` - ISO 4217 currency code for decimal validation
380///
381/// # Returns
382/// Parsed amount as f64
383///
384/// # Errors
385/// Returns error if:
386/// - Amount format is invalid
387/// - Decimal precision exceeds currency limit (C03)
388pub 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
394/// Format amount for SWIFT output with comma decimal separator
395///
396/// This function ensures SWIFT-compliant amount formatting:
397/// - Uses comma (,) as decimal separator instead of period (.)
398/// - Maintains proper decimal precision (typically 2 decimal places)
399/// - Ensures at least one digit in the integer part
400/// - Removes trailing zeros after decimal for cleaner output
401///
402/// # Arguments
403/// * `amount` - The amount to format
404/// * `decimals` - Number of decimal places (typically 2 for most currencies)
405///
406/// # Returns
407/// SWIFT-formatted amount string with comma separator
408///
409/// # Examples
410/// ```
411/// use swift_mt_message::fields::swift_utils::format_swift_amount;
412///
413/// assert_eq!(format_swift_amount(1234.56, 2), "1234,56");
414/// assert_eq!(format_swift_amount(1000.00, 2), "1000");
415/// assert_eq!(format_swift_amount(1000.50, 2), "1000,5");
416/// ```
417pub 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    // Remove trailing zeros after comma for cleaner output
422    // e.g., "1000,00" -> "1000", "1000,50" -> "1000,5"
423    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
435/// Format amount for SWIFT output with currency-specific decimal precision
436///
437/// This is a currency-aware version of format_swift_amount that automatically
438/// determines the correct number of decimal places based on the currency code.
439///
440/// # Arguments
441/// * `amount` - The amount to format
442/// * `currency` - ISO 4217 currency code
443///
444/// # Returns
445/// SWIFT-formatted amount string with currency-appropriate precision
446///
447/// # Examples
448/// ```
449/// use swift_mt_message::fields::swift_utils::format_swift_amount_for_currency;
450///
451/// assert_eq!(format_swift_amount_for_currency(1234.56, "USD"), "1234,56");
452/// assert_eq!(format_swift_amount_for_currency(1500000.0, "JPY"), "1500000");
453/// assert_eq!(format_swift_amount_for_currency(123.456, "BHD"), "123,456");
454/// ```
455pub 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
460/// Parse date in YYMMDD format
461pub 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    // Determine century: 00-49 -> 2000-2049, 50-99 -> 1950-1999
488    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
495/// Parse date in YYYYMMDD format
496pub 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
527/// Parse time in HHMM format
528pub 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
554/// Parse datetime in YYMMDDHHMM format
555pub 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
571/// Parse reference (16x - alphanumeric, max 16 chars)
572pub 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
578/// Split input at first occurrence of delimiter
579pub 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..]; // Skip the delimiter
583        (
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
596/// Split input at first newline
597pub 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..]; // Skip the newline
601        (
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
614/// Clean and normalize text (remove extra whitespace, normalize line endings)
615pub 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
624/// Validate IBAN format
625pub fn validate_iban(iban: &str) -> Result<(), ParseError> {
626    // Basic IBAN validation (simplified)
627    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    // First two characters must be country code (letters)
637    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    // Next two characters must be check digits
644    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()); // Too short
670        assert!(parse_bic("DEUTDEFFXX").is_err()); // Wrong length
671        assert!(parse_bic("1234DEFF").is_err()); // Invalid bank code
672    }
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()); // Too short
679        assert!(parse_currency("usd").is_err()); // Not uppercase
680    }
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()); // European format
697        assert!(parse_amount("1234").is_ok());
698        assert!(parse_amount("abc").is_err());
699    }
700
701    #[test]
702    fn test_format_swift_amount() {
703        // Test standard 2 decimal formatting
704        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        // Test trailing zero removal
709        assert_eq!(format_swift_amount(5000.00, 2), "5000");
710        assert_eq!(format_swift_amount(2500.00, 2), "2500");
711
712        // Test with single decimal
713        assert_eq!(format_swift_amount(250.75, 2), "250,75");
714        assert_eq!(format_swift_amount(99.99, 2), "99,99");
715
716        // Test large amounts
717        assert_eq!(format_swift_amount(1000000.0, 2), "1000000");
718        assert_eq!(format_swift_amount(1234567.89, 2), "1234567,89");
719
720        // Test zero decimals (for currencies like JPY)
721        assert_eq!(format_swift_amount(1500000.0, 0), "1500000");
722
723        // Test three decimals (for currencies like BHD)
724        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()); // Too short
743        assert!(validate_iban("1234567890123456").is_err()); // Invalid country code
744    }
745
746    #[test]
747    fn test_get_currency_decimals() {
748        // Test zero decimal currencies
749        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        // Test two decimal currencies (default)
755        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        // Test three decimal currencies
761        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        // Test four decimal currencies
767        assert_eq!(get_currency_decimals("CLF"), 4);
768
769        // Test unknown currency (defaults to 2)
770        assert_eq!(get_currency_decimals("XXX"), 2);
771    }
772
773    #[test]
774    fn test_validate_non_commodity_currency() {
775        // Valid non-commodity currencies
776        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        // Invalid commodity currencies (C08)
781        assert!(validate_non_commodity_currency("XAU").is_err()); // Gold
782        assert!(validate_non_commodity_currency("XAG").is_err()); // Silver
783        assert!(validate_non_commodity_currency("XPD").is_err()); // Palladium
784        assert!(validate_non_commodity_currency("XPT").is_err()); // Platinum
785
786        // Verify error message contains C08
787        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        // Valid non-commodity currencies
799        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        // Invalid commodity currencies
804        assert!(parse_currency_non_commodity("XAU").is_err());
805        assert!(parse_currency_non_commodity("XAG").is_err());
806
807        // Invalid format
808        assert!(parse_currency_non_commodity("US").is_err()); // Too short
809        assert!(parse_currency_non_commodity("usd").is_err()); // Lowercase
810    }
811
812    #[test]
813    fn test_validate_amount_decimals() {
814        // USD (2 decimals allowed)
815        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()); // 3 decimals
819
820        // JPY (0 decimals allowed)
821        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()); // Has decimals
824        assert!(validate_amount_decimals(100.50, "JPY").is_err()); // Has decimals
825
826        // BHD (3 decimals allowed)
827        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()); // 4 decimals
831
832        // Verify error message contains C03
833        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        // Valid amounts with correct decimals
845        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        // European format (comma separator)
856        assert_eq!(parse_amount_with_currency("100,50", "EUR").unwrap(), 100.50);
857
858        // Invalid: too many decimals
859        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        // Invalid format
864        assert!(parse_amount_with_currency("abc", "USD").is_err());
865    }
866
867    #[test]
868    fn test_format_swift_amount_for_currency() {
869        // USD (2 decimals)
870        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        // JPY (0 decimals)
875        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        // BHD (3 decimals)
882        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        // EUR (2 decimals)
887        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}