1use crate::PaymentError;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct CurrencyCode(String);
6
7impl CurrencyCode {
8 pub fn new(code: impl AsRef<str>) -> Result<Self, PaymentError> {
14 let code = code.as_ref().trim();
15 if code.len() != 3 || !code.bytes().all(|byte| byte.is_ascii_alphabetic()) {
16 return Err(PaymentError::InvalidCurrencyCode(code.to_owned()));
17 }
18
19 Ok(Self(code.to_ascii_uppercase()))
20 }
21
22 #[inline]
24 #[must_use]
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28
29 #[inline]
31 #[must_use]
32 pub fn minor_unit_exponent(&self) -> u8 {
33 match self.0.as_str() {
34 "BIF" | "CLP" | "DJF" | "GNF" | "JPY" | "KMF" | "KRW" | "MGA" | "PYG" | "RWF"
35 | "UGX" | "VND" | "VUV" | "XAF" | "XOF" | "XPF" => 0,
36 "BHD" | "IQD" | "JOD" | "KWD" | "LYD" | "OMR" | "TND" => 3,
37 "CLF" | "UYW" => 4,
38 _ => 2,
39 }
40 }
41
42 #[inline]
44 #[must_use]
45 pub fn minor_unit_factor(&self) -> i64 {
46 10_i64.pow(u32::from(self.minor_unit_exponent()))
47 }
48
49 #[must_use]
51 pub fn format_minor_units(&self, minor: i64) -> String {
52 let exponent = self.minor_unit_exponent();
53 if exponent == 0 {
54 return minor.to_string();
55 }
56
57 let factor = self.minor_unit_factor() as u64;
58 let sign = if minor < 0 { "-" } else { "" };
59 let absolute = minor.unsigned_abs();
60 let major = absolute / factor;
61 let fractional = absolute % factor;
62 let width = usize::from(exponent);
63 format!("{sign}{major}.{fractional:0width$}")
64 }
65
66 pub fn minor_units_to_major_integer(&self, minor: i64) -> Result<i64, PaymentError> {
72 let factor = self.minor_unit_factor();
73 if minor % factor != 0 {
74 return Err(PaymentError::InvalidAmount(minor));
75 }
76
77 Ok(minor / factor)
78 }
79
80 pub fn major_integer_to_minor_units(&self, major: i64) -> Result<i64, PaymentError> {
86 major
87 .checked_mul(self.minor_unit_factor())
88 .ok_or(PaymentError::InvalidAmount(major))
89 }
90}
91
92impl AsRef<str> for CurrencyCode {
93 #[inline]
94 fn as_ref(&self) -> &str {
95 self.as_str()
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn new_uppercases_valid_code() {
105 let code = CurrencyCode::new("usd").expect("currency should be valid");
106
107 assert_eq!(code.as_str(), "USD");
108 }
109
110 #[test]
111 fn new_rejects_invalid_code() {
112 assert!(matches!(
113 CurrencyCode::new("US1"),
114 Err(PaymentError::InvalidCurrencyCode(_))
115 ));
116 }
117
118 #[test]
119 fn formats_minor_units_using_currency_exponent() {
120 let usd = CurrencyCode::new("USD").expect("currency should be valid");
121 let jpy = CurrencyCode::new("JPY").expect("currency should be valid");
122 let kwd = CurrencyCode::new("KWD").expect("currency should be valid");
123
124 assert_eq!(usd.format_minor_units(1234), "12.34");
125 assert_eq!(usd.format_minor_units(-1234), "-12.34");
126 assert_eq!(jpy.format_minor_units(1234), "1234");
127 assert_eq!(kwd.format_minor_units(1234), "1.234");
128 }
129
130 #[test]
131 fn converts_major_and_minor_units_without_truncation() {
132 let zmw = CurrencyCode::new("ZMW").expect("currency should be valid");
133
134 assert_eq!(
135 zmw.minor_units_to_major_integer(5_000)
136 .expect("minor amount should convert"),
137 50
138 );
139 assert!(matches!(
140 zmw.minor_units_to_major_integer(5_050),
141 Err(PaymentError::InvalidAmount(5_050))
142 ));
143 assert_eq!(
144 zmw.major_integer_to_minor_units(50)
145 .expect("major amount should convert"),
146 5_000
147 );
148 }
149}