Skip to main content

payrail_core/
payment_method.rs

1use crate::{CountryCode, PaymentError, PhoneNumber};
2
3/// Provider-neutral payment method.
4#[derive(Debug, Clone, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum PaymentMethod {
7    /// Card payment.
8    Card(CardPaymentMethod),
9    /// Stablecoin payment.
10    Stablecoin(StablecoinPaymentMethod),
11    /// Crypto payment.
12    Crypto(CryptoPaymentMethod),
13    /// PayPal order.
14    PayPal(PayPalPaymentMethod),
15    /// Mobile Money collection.
16    MobileMoney(MobileMoneyPaymentMethod),
17}
18
19impl PaymentMethod {
20    /// Creates a card payment method.
21    #[inline]
22    #[must_use]
23    pub const fn card() -> Self {
24        Self::Card(CardPaymentMethod)
25    }
26
27    /// Creates a PayPal payment method.
28    #[inline]
29    #[must_use]
30    pub const fn paypal() -> Self {
31        Self::PayPal(PayPalPaymentMethod)
32    }
33
34    /// Creates a stablecoin payment method for an asset.
35    #[inline]
36    #[must_use]
37    pub fn stablecoin(asset: StablecoinAsset) -> Self {
38        Self::Stablecoin(StablecoinPaymentMethod {
39            preferred_asset: Some(asset),
40        })
41    }
42
43    /// Creates a USDC stablecoin method.
44    #[inline]
45    #[must_use]
46    pub fn stablecoin_usdc() -> Self {
47        Self::stablecoin(StablecoinAsset::Usdc)
48    }
49
50    /// Creates a USDT stablecoin method.
51    #[inline]
52    #[must_use]
53    pub fn stablecoin_usdt() -> Self {
54        Self::stablecoin(StablecoinAsset::Usdt)
55    }
56
57    /// Creates a crypto payment method for an asset.
58    #[inline]
59    #[must_use]
60    pub const fn crypto(asset: CryptoAsset) -> Self {
61        Self::Crypto(CryptoPaymentMethod {
62            asset,
63            network: None,
64        })
65    }
66
67    /// Creates a crypto payment method for an asset on a specific network.
68    #[inline]
69    #[must_use]
70    pub const fn crypto_on(asset: CryptoAsset, network: CryptoNetwork) -> Self {
71        Self::Crypto(CryptoPaymentMethod {
72            asset,
73            network: Some(network),
74        })
75    }
76
77    /// Creates a USDC crypto payment method on a specific network.
78    #[inline]
79    #[must_use]
80    pub const fn usdc_on(network: CryptoNetwork) -> Self {
81        Self::crypto_on(CryptoAsset::Usdc, network)
82    }
83
84    /// Creates a USDT crypto payment method on a specific network.
85    #[inline]
86    #[must_use]
87    pub const fn usdt_on(network: CryptoNetwork) -> Self {
88        Self::crypto_on(CryptoAsset::Usdt, network)
89    }
90
91    /// Creates a Zambia Mobile Money payment method.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error when the phone number or country code is invalid.
96    pub fn mobile_money_zambia(phone_number: impl AsRef<str>) -> Result<Self, PaymentError> {
97        Ok(Self::MobileMoney(MobileMoneyPaymentMethod {
98            country: CountryCode::new("ZM")?,
99            phone_number: PhoneNumber::new(phone_number)?,
100            operator: None,
101        }))
102    }
103}
104
105/// Card payment marker.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub struct CardPaymentMethod;
108
109/// PayPal payment marker.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub struct PayPalPaymentMethod;
112
113/// Stablecoin payment configuration.
114#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
115pub struct StablecoinPaymentMethod {
116    /// Preferred stablecoin asset.
117    pub preferred_asset: Option<StablecoinAsset>,
118}
119
120/// Stablecoin asset.
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122#[non_exhaustive]
123pub enum StablecoinAsset {
124    /// USDC.
125    Usdc,
126    /// USDT.
127    Usdt,
128    /// USDP.
129    Usdp,
130    /// USDG.
131    Usdg,
132    /// Other stablecoin asset.
133    Other(String),
134}
135
136/// Crypto payment configuration.
137#[derive(Debug, Clone, PartialEq, Eq, Hash)]
138pub struct CryptoPaymentMethod {
139    /// Requested crypto asset.
140    pub asset: CryptoAsset,
141    /// Optional requested network.
142    pub network: Option<CryptoNetwork>,
143}
144
145/// Crypto asset.
146#[derive(Debug, Clone, PartialEq, Eq, Hash)]
147#[non_exhaustive]
148pub enum CryptoAsset {
149    /// Bitcoin.
150    Btc,
151    /// Ether.
152    Eth,
153    /// Solana.
154    Sol,
155    /// USDC.
156    Usdc,
157    /// USDT.
158    Usdt,
159    /// USDP.
160    Usdp,
161    /// USDG.
162    Usdg,
163    /// Other crypto asset.
164    Other(String),
165}
166
167/// Crypto settlement network.
168#[derive(Debug, Clone, PartialEq, Eq, Hash)]
169#[non_exhaustive]
170pub enum CryptoNetwork {
171    /// Bitcoin network.
172    Bitcoin,
173    /// Ethereum mainnet.
174    Ethereum,
175    /// Solana.
176    Solana,
177    /// Polygon.
178    Polygon,
179    /// Base.
180    Base,
181    /// BNB Smart Chain.
182    Bsc,
183    /// Arbitrum.
184    Arbitrum,
185    /// Optimism.
186    Optimism,
187    /// Other crypto network.
188    Other(String),
189}
190
191impl From<&StablecoinAsset> for CryptoAsset {
192    fn from(asset: &StablecoinAsset) -> Self {
193        match asset {
194            StablecoinAsset::Usdc => Self::Usdc,
195            StablecoinAsset::Usdt => Self::Usdt,
196            StablecoinAsset::Usdp => Self::Usdp,
197            StablecoinAsset::Usdg => Self::Usdg,
198            StablecoinAsset::Other(asset) => Self::Other(asset.clone()),
199        }
200    }
201}
202
203/// Mobile Money payment configuration.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct MobileMoneyPaymentMethod {
206    /// Country for the Mobile Money account.
207    pub country: CountryCode,
208    /// Customer phone number.
209    pub phone_number: PhoneNumber,
210    /// Optional operator hint.
211    pub operator: Option<MobileMoneyOperator>,
212}
213
214/// Mobile Money operator.
215#[derive(Debug, Clone, PartialEq, Eq)]
216#[non_exhaustive]
217pub enum MobileMoneyOperator {
218    /// MTN.
219    Mtn,
220    /// Airtel.
221    Airtel,
222    /// Zamtel.
223    Zamtel,
224    /// M-Pesa.
225    Mpesa,
226    /// Orange Money.
227    Orange,
228    /// Other operator.
229    Other(String),
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn mobile_money_zambia_sets_country() {
238        let method =
239            PaymentMethod::mobile_money_zambia("260971234567").expect("method should be valid");
240
241        match method {
242            PaymentMethod::MobileMoney(method) => {
243                assert_eq!(method.country.as_str(), "ZM");
244                assert_eq!(method.phone_number.digits(), "260971234567");
245            }
246            PaymentMethod::Card(_)
247            | PaymentMethod::Stablecoin(_)
248            | PaymentMethod::Crypto(_)
249            | PaymentMethod::PayPal(_) => panic!("expected mobile money"),
250        }
251    }
252
253    #[test]
254    fn helper_constructors_create_expected_variants() {
255        assert!(matches!(PaymentMethod::card(), PaymentMethod::Card(_)));
256        assert!(matches!(PaymentMethod::paypal(), PaymentMethod::PayPal(_)));
257        assert!(matches!(
258            PaymentMethod::stablecoin_usdc(),
259            PaymentMethod::Stablecoin(StablecoinPaymentMethod {
260                preferred_asset: Some(StablecoinAsset::Usdc)
261            })
262        ));
263        assert!(matches!(
264            PaymentMethod::stablecoin_usdt(),
265            PaymentMethod::Stablecoin(StablecoinPaymentMethod {
266                preferred_asset: Some(StablecoinAsset::Usdt)
267            })
268        ));
269        assert!(matches!(
270            PaymentMethod::usdc_on(CryptoNetwork::Base),
271            PaymentMethod::Crypto(CryptoPaymentMethod {
272                asset: CryptoAsset::Usdc,
273                network: Some(CryptoNetwork::Base)
274            })
275        ));
276        assert!(matches!(
277            PaymentMethod::usdt_on(CryptoNetwork::Solana),
278            PaymentMethod::Crypto(CryptoPaymentMethod {
279                asset: CryptoAsset::Usdt,
280                network: Some(CryptoNetwork::Solana)
281            })
282        ));
283    }
284
285    #[test]
286    fn stablecoin_assets_convert_to_crypto_assets() {
287        assert_eq!(CryptoAsset::from(&StablecoinAsset::Usdc), CryptoAsset::Usdc);
288        assert_eq!(CryptoAsset::from(&StablecoinAsset::Usdt), CryptoAsset::Usdt);
289        assert_eq!(
290            CryptoAsset::from(&StablecoinAsset::Other("eurc".to_owned())),
291            CryptoAsset::Other("eurc".to_owned())
292        );
293    }
294}