Skip to main content

emv_3ds/types/
amount.rs

1use crate::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3
4/// ISO 4217 currency code (numeric, zero-padded to 3 digits).
5#[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    /// Returns the zero-padded 3-digit string as required by the EMVCo spec.
39    pub fn as_spec_string(self) -> String {
40        format!("{:03}", self.0)
41    }
42}
43
44/// A monetary amount expressed in minor units (e.g. pence, cents) with
45/// a currency and the number of minor-unit decimal places.
46///
47/// The EMVCo spec requires `purchaseAmount` as a numeric string (max 48 digits)
48/// and `purchaseExponent` as the number of digits after the decimal point.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct Amount {
51    /// Minor-unit value (e.g. 1099 for £10.99)
52    pub minor_units: u64,
53    pub currency: Currency,
54    /// Decimal exponent per ISO 4217 (e.g. 2 for GBP → £X.XX)
55    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    /// Convenience: construct from major units with a 2-digit exponent.
68    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    /// EMVCo `purchaseAmount` field value.
77    pub fn spec_amount(&self) -> String {
78        self.minor_units.to_string()
79    }
80
81    /// EMVCo `purchaseCurrency` field value.
82    pub fn spec_currency(&self) -> String {
83        self.currency.as_spec_string()
84    }
85
86    /// EMVCo `purchaseExponent` field value.
87    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}