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#[derive(Debug, Deserialize, Serialize)]
18pub struct PaymentBuilder {
19 amount: f64,
21 email: String,
22 key: String,
23
24 #[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 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 pub fn build(self) -> Payment {
72 Payment(self)
73 }
74
75 pub fn amount(&self) -> f64 {
77 self.amount
78 }
79
80 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 pub fn metadata(&mut self, metadata: String) {
93 self.metadata = Some(metadata)
94 }
95
96 pub fn label(&mut self, label: String) {
98 self.label = Some(label)
99 }
100
101 pub fn reference(&mut self, reference: String) {
103 self.reference = Some(reference)
104 }
105
106 pub fn mobile_money(&mut self, mobile_money: MobileMoney) {
108 self.mobile_money = Some(mobile_money)
109 }
110
111 pub fn card(&mut self, card: Card) {
113 self.card = Some(card)
114 }
115
116 pub fn bank(&mut self, bank: Bank) {
118 self.bank = Some(bank)
119 }
120
121 pub fn bank_transfer(&mut self, bank_transfer: BankTransfer) {
123 self.bank_transfer = Some(bank_transfer)
124 }
125
126 pub fn ussd(&mut self, ussd: Ussd) {
128 self.ussd = Some(ussd)
129 }
130
131 pub fn qr(&mut self, qr: QR) {
133 self.qr = Some(qr)
134 }
135
136 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
161pub struct Payment(PaymentBuilder);
163
164impl Payment {
165 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 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#[derive(Debug, Serialize, Deserialize)]
236pub enum Currency {
237 NGN,
239 USD,
241 GHS,
243 ZAR,
245 KES,
247}
248
249#[derive(Debug, Serialize, Deserialize)]
251pub enum Channel {
252 Card,
253 Bank,
254 Ussd,
255 QR,
256 MobileMoney,
257 BankTransfer,
258}
259
260mod 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}