midtransclient/
snap.rs

1//! Snap
2
3use std::collections::HashMap;
4use reqwest::{Method, header::HeaderMap, Proxy};
5use serde_json::Value;
6use crate::{ApiConfig, MidtransError, http_client::MidtransClient, Transactions};
7
8type MidtransResult = Result<HashMap<String, Value>, MidtransError>;
9
10/// Snap struct used to do request to Midtrans Snap API
11pub struct Snap {
12    pub api_config: ApiConfig,
13}
14
15impl MidtransClient for Snap {}
16
17impl Transactions for Snap {
18    /// Getter for ApiConfig
19    fn get_api_config(&self) -> &ApiConfig {
20        &self.api_config
21    }
22
23    /// Setter for ApiConfig
24    fn set_api_config(&mut self, api_config: ApiConfig) {
25        self.api_config = api_config
26    }
27}
28
29pub struct SnapBuilder {
30    is_production: bool,
31    server_key: String,
32    client_key: Option<String>,
33    custom_headers: Option<HeaderMap>,
34    proxies: Option<Proxy>
35}
36
37impl SnapBuilder {
38    pub fn client_key(&mut self, client_key: String) -> &mut Self {
39        self.client_key = Some(client_key);
40        self
41    }
42
43    pub fn custom_headers(&mut self, custom_headers: HeaderMap) -> &mut Self {
44        self.custom_headers = Some(custom_headers);
45        self
46    }
47
48    pub fn proxies(&mut self, proxies: Proxy) -> &mut Self {
49        self.proxies = Some(proxies);
50        self
51    }
52
53    pub fn build(&self) -> Result<Snap, MidtransError> {
54        let mut api_config = ApiConfig::new(self.is_production, self.server_key.clone());
55
56        if let Some(key) = &self.client_key {
57            api_config.client_key(key.clone());
58        }
59
60        if let Some(headers) = &self.custom_headers {
61            api_config.custom_header(headers.clone());
62        }
63
64        if let Some(proxy) = &self.proxies {
65            api_config.proxies(proxy.clone());
66        }
67
68        let api_config = api_config.build();
69
70        Ok(Snap { api_config })
71    }
72}
73
74impl Snap {
75    pub fn new(is_production: bool, server_key: String) -> SnapBuilder {
76        SnapBuilder {
77            is_production,
78            server_key,
79            client_key: None,
80            custom_headers: None,
81            proxies: None
82        }
83    }
84
85    /// Trigger API call to Snap API
86    ///
87    /// ### Argument
88    ///
89    /// `parameters` is a `&str` of Core API JSON
90    /// (more params detail refer to: <https://snap-docs.midtrans.com>)
91    ///
92    /// ### Return
93    ///
94    /// HashMap from JSON decoded response, that contains `token` and `redirect_url`
95    ///
96    pub fn create_transaction(&self, parameters: &str) -> MidtransResult {
97        let api_url = format!(
98            "{}/snap/v1/transactions",
99            self.api_config.get_snap_base_url()
100        );
101
102        let response = self.request(
103            Method::POST,
104            self.api_config.get_server_key(),
105            &api_url,
106            parameters,
107            self.api_config.get_custom_headers().clone(),
108            self.api_config.get_proxies().clone()
109        )?;
110
111        Ok(response)
112    }
113
114    /// Wrapper method that call `create_transaction` and directly return `token`
115    pub fn create_transaction_token(&self, parameters: &str) -> Result<Value, MidtransError> {
116        let response = self.create_transaction(parameters)?;
117        Ok(response["token"].clone())
118    }
119
120    /// Wrapper method that call `create_transaction` and directly return `redirect_url`
121    pub fn create_transaction_redirect_url(&self, parameters: &str) -> Result<Value, MidtransError> {
122        let response = self.create_transaction(parameters)?;
123        Ok(response["redirect_url"].clone())
124    }
125
126}
127
128#[cfg(test)]
129mod test {
130    // When you run multiple tests, by default they run in parallel using threads,
131    // meaning they finish running faster and you get feedback quicker.
132    // Because the tests are running at the same time, you must make sure
133    // your tests don’t depend on each other or on any shared state,
134    // including a shared environment, such as the current working directory
135    // or environment variables.
136    //
137    // If you don’t want to run the tests in parallel or if you want more
138    // fine-grained control over the number of threads used, you can send
139    // the --test-threads flag and the number of threads you want to use
140    // to the test binary. Take a look at the following example:
141    //
142    // $ cargo test -- --test-threads=1
143    //
144    // source: https://doc.rust-lang.org/book/ch11-02-running-tests.html
145
146    use super::*;
147    use serde_json::json;
148
149    mod helper {
150        use super::*;
151        use std::env;
152        use chrono;
153
154        pub(crate) fn server_key() -> String {
155            env::var("MIDTRANS_SERVER_KEY").expect("SERVER_KEY NOT FOUND")
156        }
157
158        pub(crate) fn client_key() -> String {
159            env::var("MIDTRANS_CLIENT_KEY").expect("CLIENT_KEY NOT FOUND")
160        }
161
162        pub(crate) fn generate_snap_api_instance() -> Snap {
163            Snap::new(false, server_key())
164                .client_key(client_key())
165                .build()
166                .unwrap()
167        }
168
169        pub(crate) fn generate_order_id(test_number: u8) -> String {
170            let now = chrono::offset::Local::now().format("%Y%m%d%H%M%S").to_string();
171            format!("rust-midtransclient-test{}-{}", test_number, now)
172        }
173
174        pub(crate) fn generate_param_min(order_id: &str) -> String {
175            json!({
176                "transaction_details": {
177                    "order_id": order_id,
178                    "gross_amount": 200000
179                }, "credit_card":{
180                    "secure" : true
181                }
182            }).to_string()
183        }
184
185        pub(crate) fn generate_param_max(order_id: &str) -> String {
186            json!({
187                "transaction_details": {
188                    "order_id": order_id,
189                    "gross_amount": 10000
190                },
191                "item_details": [{
192                    "id": "ITEM1",
193                    "price": 10000,
194                    "quantity": 1,
195                    "name": "Midtrans Bear",
196                    "brand": "Midtrans",
197                    "category": "Toys",
198                    "merchant_name": "Midtrans"
199                }],
200                "customer_details": {
201                    "first_name": "John",
202                    "last_name": "Watson",
203                    "email": "test@example.com",
204                    "phone": "+628123456",
205                    "billing_address": {
206                        "first_name": "John",
207                        "last_name": "Watson",
208                        "email": "test@example.com",
209                        "phone": "081 2233 44-55",
210                        "address": "Sudirman",
211                        "city": "Jakarta",
212                        "postal_code": "12190",
213                        "country_code": "IDN"
214                    },
215                    "shipping_address": {
216                        "first_name": "John",
217                        "last_name": "Watson",
218                        "email": "test@example.com",
219                        "phone": "0 8128-75 7-9338",
220                        "address": "Sudirman",
221                        "city": "Jakarta",
222                        "postal_code": "12190",
223                        "country_code": "IDN"
224                    }
225                },
226                "enabled_payments": ["credit_card", "mandiri_clickpay", "cimb_clicks","bca_klikbca", "bca_klikpay", "bri_epay", "echannel", "indosat_dompetku","mandiri_ecash", "permata_va", "bca_va", "bni_va", "other_va", "gopay","kioson", "indomaret", "gci", "danamon_online"],
227                "credit_card": {
228                    "secure": true,
229                    "channel": "migs",
230                    "bank": "bca",
231                    "installment": {
232                        "required": false,
233                        "terms": {
234                            "bni": [3, 6, 12],
235                            "mandiri": [3, 6, 12],
236                            "cimb": [3],
237                            "bca": [3, 6, 12],
238                            "offline": [6, 12]
239                        }
240                    },
241                    "whitelist_bins": [
242                        "48111111",
243                        "41111111"
244                    ]
245                },
246                "bca_va": {
247                    "va_number": "12345678911",
248                    "free_text": {
249                        "inquiry": [
250                            {
251                                "en": "text in English",
252                                "id": "text in Bahasa Indonesia"
253                            }
254                        ],
255                        "payment": [
256                            {
257                                "en": "text in English",
258                                "id": "text in Bahasa Indonesia"
259                            }
260                        ]
261                    }
262                },
263                "bni_va": {
264                    "va_number": "12345678"
265                },
266                "permata_va": {
267                    "va_number": "1234567890",
268                    "recipient_name": "SUDARSONO"
269                },
270                "callbacks": {
271                    "finish": "https://demo.midtrans.com"
272                },
273                "expiry": {
274                    "start_time": "2030-12-20 18:11:08 +0700",
275                    "unit": "minutes",
276                    "duration": 1
277                },
278                "custom_field1": "custom field 1 content",
279                "custom_field2": "custom field 2 content",
280                "custom_field3": "custom field 3 content"
281            }).to_string()
282        }
283    }
284
285    mod snap {
286        use super::*;
287        use super::helper::*;
288
289        #[test]
290        fn new() {
291            let snap = Snap::new(false, "server_key".to_string()).build().unwrap();
292            assert_eq!(snap.api_config.get_is_production(), false);
293            assert_eq!(snap.api_config.get_server_key(), "server_key");
294            assert_eq!(snap.api_config.get_client_key(), "");
295            assert!(snap.api_config.get_custom_headers().is_none());
296            assert!(snap.api_config.get_proxies().is_none());
297        }
298
299        #[test]
300        fn new_with_optionals() {
301            let is_production = false;
302            let server_key = String::from("server_key");
303            let client_key = String::from("client_key");
304            let mut custom_headers = HeaderMap::new();
305            let proxies = reqwest::Proxy::http("https://secure.example").unwrap();
306            custom_headers.insert("X-Custom-Header", "Some Value".parse().unwrap());
307            let snap = Snap::new(is_production, server_key)
308                .client_key(client_key)
309                .custom_headers(custom_headers.clone())
310                .proxies(proxies)
311                .build()
312                .unwrap();
313            assert_eq!(snap.api_config.get_is_production(), false);
314            assert_eq!(snap.api_config.get_server_key(), "server_key");
315            assert_eq!(snap.api_config.get_client_key(), "client_key");
316            assert_eq!(snap.api_config.get_custom_headers().clone().unwrap(), custom_headers);
317            assert!(!snap.api_config.get_proxies().is_none());
318        }
319
320        #[test]
321        fn create_transaction_min() -> Result<(), MidtransError> {
322            let snap = generate_snap_api_instance();
323            let order_id = generate_order_id(1);
324            let parameters = generate_param_min(&order_id);
325            let transaction = snap.create_transaction(&parameters)?;
326            assert!(transaction.contains_key("token"));
327            assert!(transaction.contains_key("redirect_url"));
328            Ok(())
329        }
330
331        #[test]
332        fn create_transaction_max() -> Result<(), MidtransError> {
333            let snap = generate_snap_api_instance();
334            let order_id = generate_order_id(1);
335            let parameters = generate_param_max(&order_id);
336            let transaction = snap.create_transaction(&parameters)?;
337            assert!(transaction.contains_key("token"));
338            assert!(transaction.contains_key("redirect_url"));
339            Ok(())
340        }
341
342        #[test]
343        fn create_transaction_token() -> Result<(), MidtransError> {
344            let snap = generate_snap_api_instance();
345            let order_id = generate_order_id(1);
346            let parameters = generate_param_min(&order_id);
347            let token = snap.create_transaction_token(&parameters)?;
348            assert!(token.to_string().len() > 0);
349            Ok(())
350        }
351
352        #[test]
353        fn create_transaction_redirect_url() -> Result<(), MidtransError> {
354            let snap = generate_snap_api_instance();
355            let order_id = generate_order_id(1);
356            let parameters = generate_param_min(&order_id);
357            let redirect_url = snap.create_transaction_redirect_url(&parameters)?;
358            assert!(redirect_url.to_string().len() > 0);
359            Ok(())
360        }
361
362        #[test]
363        fn status_fail_404() -> Result<(), MidtransError> {
364            let snap = generate_snap_api_instance();
365            let response = snap.status("non-exist-order-id".to_string());
366            assert!(response.is_err());
367            if let Err(MidtransError::ApiError(e)) = response {
368                assert_eq!(e.status_code, 404);
369            }
370            Ok(())
371        }
372
373        #[test]
374        fn status_fail_401() -> Result<(), MidtransError> {
375            let mut snap = generate_snap_api_instance();
376            snap.api_config.set_server_key("dummy".to_string());
377            let order_id = generate_order_id(1);
378            let parameters = generate_param_min(&order_id);
379            let transaction = snap.create_transaction(&parameters);
380            assert!(transaction.is_err());
381            if let Err(MidtransError::ApiError(e)) = transaction {
382                assert_eq!(e.status_code, 401);
383                assert!(e.message.contains("unauthorized"));
384            }
385            Ok(())
386        }
387
388        #[test]
389        fn charge_fail_empty_param() -> Result<(), MidtransError> {
390            let snap = generate_snap_api_instance();
391            let parameters = String::from("");
392            let response = snap.create_transaction(&parameters);
393            assert!(response.is_err());
394            if let Err(MidtransError::ApiError(e)) = response {
395                assert_eq!(e.status_code, 400);
396            }
397            Ok(())
398        }
399
400        #[test]
401        fn charge_fail_zero_gross_amount() -> Result<(), MidtransError> {
402            let snap = generate_snap_api_instance();
403            let order_id = generate_order_id(1);
404            let parameters = json!({
405                "transaction_details": {
406                    "order_id": order_id,
407                    "gross_amount": 0
408                }, "credit_card":{
409                    "secure" : true
410                }
411            }).to_string();
412            let response = snap.create_transaction(&parameters);
413            assert!(response.is_err());
414            if let Err(MidtransError::ApiError(e)) = response {
415                assert_eq!(e.status_code, 400);
416            }
417            Ok(())
418        }
419
420        #[test]
421        fn exception_midtrans_api_error() -> Result<(), MidtransError> {
422            let mut snap = generate_snap_api_instance();
423            snap.api_config.set_server_key("dummy".to_string());
424            let order_id = generate_order_id(1);
425            let parameters = generate_param_min(&order_id);
426            let transaction = snap.create_transaction(&parameters);
427            assert!(transaction.is_err());
428            if let Err(MidtransError::ApiError(e)) = transaction {
429                assert!(e.message.contains("Midtrans API is returning API error."));
430                assert_eq!(e.status_code, 401);
431                assert_eq!(e.response["status_code"], "401");
432            }
433            Ok(())
434        }
435
436        #[test]
437        fn create_transaction_min_with_custom_headers_via_setter() -> Result<(), MidtransError> {
438            let mut snap = generate_snap_api_instance();
439            let mut headers = HeaderMap::new();
440            headers.insert("X-Override-Notification", "https://example.org".parse().unwrap());
441            snap.api_config.set_custom_headers(headers);
442            let order_id = generate_order_id(1);
443            let parameters = generate_param_min(&order_id);
444            let transaction = snap.create_transaction(&parameters)?;
445            assert!(transaction.contains_key("token"));
446            assert!(transaction.contains_key("redirect_url"));
447            Ok(())
448        }
449    }
450}