1use crate::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(transparent)]
7pub struct Currency(u16);
8
9impl Currency {
10 pub const GBP: Self = Self(826);
11 pub const USD: Self = Self(840);
12 pub const EUR: Self = Self(978);
13 pub const NGN: Self = Self(566);
14 pub const ETB: Self = Self(230);
15 pub const KES: Self = Self(404);
16 pub const GHS: Self = Self(936);
17 pub const INR: Self = Self(356);
18 pub const PHP: Self = Self(608);
19 pub const MXN: Self = Self(484);
20 pub const MAD: Self = Self(504);
21 pub const AED: Self = Self(784);
22 pub const SAR: Self = Self(682);
23
24 pub fn new(code: u16) -> Result<Self> {
25 if code > 999 {
26 return Err(Error::InvalidField {
27 field: "currency",
28 reason: format!("ISO 4217 numeric code must be 0–999, got {code}"),
29 });
30 }
31 Ok(Self(code))
32 }
33
34 pub fn code(self) -> u16 {
35 self.0
36 }
37
38 pub fn as_spec_string(self) -> String {
40 format!("{:03}", self.0)
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct Amount {
51 pub minor_units: u64,
53 pub currency: Currency,
54 pub exponent: u8,
56}
57
58impl Amount {
59 pub fn new(minor_units: u64, currency: Currency, exponent: u8) -> Self {
60 Self {
61 minor_units,
62 currency,
63 exponent,
64 }
65 }
66
67 pub fn from_major(major: u64, currency: Currency) -> Self {
69 Self {
70 minor_units: major * 100,
71 currency,
72 exponent: 2,
73 }
74 }
75
76 pub fn spec_amount(&self) -> String {
78 self.minor_units.to_string()
79 }
80
81 pub fn spec_currency(&self) -> String {
83 self.currency.as_spec_string()
84 }
85
86 pub fn spec_exponent(&self) -> String {
88 self.exponent.to_string()
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn currency_spec_string_zero_pads() {
98 assert_eq!(Currency::new(826).unwrap().as_spec_string(), "826");
99 assert_eq!(Currency::new(8).unwrap().as_spec_string(), "008");
100 assert_eq!(Currency::new(78).unwrap().as_spec_string(), "078");
101 }
102
103 #[test]
104 fn currency_rejects_out_of_range() {
105 assert!(Currency::new(1000).is_err());
106 }
107
108 #[test]
109 fn amount_spec_fields() {
110 let a = Amount::from_major(10, Currency::GBP);
111 assert_eq!(a.spec_amount(), "1000");
112 assert_eq!(a.spec_currency(), "826");
113 assert_eq!(a.spec_exponent(), "2");
114 }
115}