paystack_transaction/
initialize.rs

1use std::time;
2
3use async_trait::async_trait;
4use reqwest::Client;
5use secrecy::Secret;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::{
10    channels::{Bank, BankTransfer, Card, MobileMoney, Ussd, QR},
11    expose_secret,
12    verify::{VerificationData, Verify},
13    ResponseError,
14};
15
16/// Building blocks for initiating a Paystack Payment
17#[derive(Debug, Deserialize, Serialize)]
18pub struct PaymentBuilder {
19    // Required Data
20    amount: f64,
21    email: String,
22    key: String,
23
24    //Channel Options
25    #[serde(skip_serializing_if = "Option::is_none")]
26    bank: Option<Bank>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    bank_transfer: Option<BankTransfer>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    card: Option<Card>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    mobile_money: Option<MobileMoney>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    qr: Option<QR>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    ussd: Option<Ussd>,
37
38    #[serde(skip_serializing_if = "Option::is_none")]
39    currency: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    label: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    metadata: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    reference: Option<String>,
46}
47
48impl PaymentBuilder {
49    /// Initiate your `PaymentBuilder` taking in the basic requirements as args.
50    ///
51    /// key can be derived using the `cred_from_env`
52    pub fn init_payment(email: String, amount: f64, key: Secret<String>) -> Self {
53        Self {
54            amount,
55            email,
56            key: expose_secret(key),
57            currency: None,
58            label: None,
59            metadata: None,
60            reference: None,
61            bank: None,
62            bank_transfer: None,
63            card: None,
64            mobile_money: None,
65            qr: None,
66            ussd: None,
67        }
68    }
69
70    /// create your `Payment` to initiate Paystack payment
71    pub fn build(self) -> Payment {
72        Payment(self)
73    }
74
75    /// Amount in the subunit of the supported currency you are debiting customer. Do not pass this if creating subscriptions.
76    pub fn amount(&self) -> f64 {
77        self.amount
78    }
79
80    /// On of the supported currency [ `NGN`, `USD`, `GHS`, `ZAR`, `KES`]. The charge should be performed in. It defaults to your integration currency.
81    pub fn currency(&mut self, currency: Currency) {
82        match currency {
83            Currency::GHS => self.currency = Some("GHS".to_string()),
84            Currency::NGN => self.currency = Some("NGN".to_string()),
85            Currency::USD => self.currency = Some("USD".to_string()),
86            Currency::ZAR => self.currency = Some("ZAR".to_string()),
87            Currency::KES => self.currency = Some("KES".to_string()),
88        }
89    }
90
91    /// Object containing any extra information you want recorded with the transaction. Fields within the custom_field object will show up on merchant receipt and within the transaction information on the Paystack Dashboard.
92    pub fn metadata(&mut self, metadata: String) {
93        self.metadata = Some(metadata)
94    }
95
96    /// String that replaces customer email as shown on the checkout form
97    pub fn label(&mut self, label: String) {
98        self.label = Some(label)
99    }
100
101    /// Unique case sensitive transaction reference. Only -,., =and alphanumeric characters allowed. If you do not pass this parameter, Paystack will generate a unique reference for you.
102    pub fn reference(&mut self, reference: String) {
103        self.reference = Some(reference)
104    }
105
106    /// Set your mobile money data
107    pub fn mobile_money(&mut self, mobile_money: MobileMoney) {
108        self.mobile_money = Some(mobile_money)
109    }
110
111    /// Set your card data
112    pub fn card(&mut self, card: Card) {
113        self.card = Some(card)
114    }
115
116    /// Set your bank data
117    pub fn bank(&mut self, bank: Bank) {
118        self.bank = Some(bank)
119    }
120
121    /// Set your bank transfer data
122    pub fn bank_transfer(&mut self, bank_transfer: BankTransfer) {
123        self.bank_transfer = Some(bank_transfer)
124    }
125
126    /// Set your ussd data
127    pub fn ussd(&mut self, ussd: Ussd) {
128        self.ussd = Some(ussd)
129    }
130
131    /// Set your qr data
132    pub fn qr(&mut self, qr: QR) {
133        self.qr = Some(qr)
134    }
135
136    // Convert this to trait, making it compatible with JavaScript functions
137    /// Add runtime funtions such as `callback`, `onClose` and `onBankTransferConfirmationPending`
138    pub fn add_fallback(&self, f: fn() -> ()) {
139        f()
140    }
141
142    fn json_builder(&self) -> serde_json::Value {
143        let mut json = serde_json::to_value(self).unwrap();
144
145        if let Value::Object(ref mut map) = json {
146            let keys_to_remove: Vec<String> = map
147                .iter()
148                .filter(|&(_, v)| v.is_null())
149                .map(|(k, _)| k.clone())
150                .collect();
151
152            for k in keys_to_remove {
153                map.remove(&k);
154            }
155        }
156
157        json
158    }
159}
160
161/// Data wrapper for payment ready to send for initialization
162pub struct Payment(PaymentBuilder);
163
164impl Payment {
165    /// Build your `PaymentBuilder` object to be used to by `Payment` to initiate Paystack payment
166    pub fn builder(email: String, amount: f64, key: Secret<String>) -> PaymentBuilder {
167        PaymentBuilder {
168            amount,
169            email,
170            key: expose_secret(key),
171            currency: None,
172            label: None,
173            metadata: None,
174            reference: None,
175            bank: None,
176            bank_transfer: None,
177            card: None,
178            mobile_money: None,
179            qr: None,
180            ussd: None,
181        }
182    }
183
184    /// Send Transaction
185    pub async fn send(&self) -> Result<(), ResponseError> {
186        let timeout = time::Duration::from_millis(10000);
187        let http_client = Client::builder().timeout(timeout).build().unwrap();
188
189        let data = self.0.json_builder();
190
191        http_client
192            .post("https://api.paystack.co/transaction/initialize")
193            .header("Authorization", format!("Bearer {}", self.0.key))
194            .header("Accept", "application/json")
195            .header("Content-Type", "application/json")
196            .header("Cache-Control", "no-cache")
197            .json(&data)
198            .send()
199            .await
200            .map_err(|e| ResponseError::PayStackError(e.to_string()))
201            .unwrap();
202
203        Ok(())
204    }
205}
206
207#[async_trait]
208impl Verify for Payment {
209    async fn verify_transaction(
210        &self,
211        reference: String,
212    ) -> Result<VerificationData, ResponseError> {
213        let timeout = time::Duration::from_millis(10000);
214        let http_client = Client::builder().timeout(timeout).build().unwrap();
215
216        let url = format!("https://api.paystack.co/transaction/verify/{reference}");
217
218        let response = http_client
219            .get(url)
220            .header("Authorization", format!("Bearer {}", self.0.key))
221            .header("Accept", "application/json")
222            .header("Content-Type", "application/json")
223            .header("Cache-Control", "no-cache")
224            .send()
225            .await
226            .unwrap();
227
228        let json_data: VerificationData = response.json().await.unwrap();
229
230        Ok(json_data)
231    }
232}
233
234/// Supported Currencies
235#[derive(Debug, Serialize, Deserialize)]
236pub enum Currency {
237    /// Nigerian Naira
238    NGN,
239    /// US Dollars
240    USD,
241    /// Ghanaian Cedis
242    GHS,
243    /// South African Rand
244    ZAR,
245    /// Kenyan Shillings
246    KES,
247}
248
249/// Available payment channels
250#[derive(Debug, Serialize, Deserialize)]
251pub enum Channel {
252    Card,
253    Bank,
254    Ussd,
255    QR,
256    MobileMoney,
257    BankTransfer,
258}
259
260// impl Channel {
261//     /// An array of payment channels to control what channels you want to make available to the user to make a payment with. Available channels include; ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer']
262//     pub fn channel(&self) -> Channel {
263//         match &self {
264//             Channel::Card => todo!(),
265//             Channel::Bank => todo!(),
266//             Channel::USSD => todo!(),
267//             Channel::QR => todo!(),
268//             Channel::MobileMoney => todo!(),
269//             Channel::BankTransfer => todo!(),
270//         }
271//     }
272
273//     pub fn mobile_money(&mut self, phone: String, provider: String) -> MobileMoney {
274//         todo!()
275//     }
276
277//     pub fn card(&mut self, card: Card) {}
278
279//     pub fn bank(&mut self, bank: Bank) {}
280
281//     pub fn bank_transfer(&mut self, bank_transfer: BankTransfer) {}
282
283//     pub fn ussd(&mut self, ussd: USSD) {}
284
285//     pub fn qr(&mut self, qr: QR) {}
286// }
287
288mod test {
289    #[test]
290    fn json_response() {
291        let mut builder = crate::Payment::builder(
292            "test@example.com".to_string(),
293            100.0,
294            "secret_key".to_string().into(),
295        );
296
297        builder.mobile_money(crate::channels::MobileMoney {
298            phone: "08123456789".to_string(),
299            provider: "MTN".to_string(),
300        });
301
302        builder.label("label".to_string());
303        builder.reference("reference".to_string());
304        builder.currency(crate::Currency::NGN);
305
306        let json_builder = builder.json_builder();
307
308        let data = r#"{
309            "amount":100.0,
310            "email":"test@example.com",
311            "mobile_money": {
312                "phone": "08123456789",
313                "provider": "MTN"
314            },
315            "currency": "NGN",
316            "key":"secret_key",
317            "label":"label",
318            "reference":"reference"
319        }"#;
320
321        let json: serde_json::Value = serde_json::from_str(data).unwrap();
322
323        assert_eq!(json, json_builder)
324    }
325}