1use 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
10pub struct Snap {
12 pub api_config: ApiConfig,
13}
14
15impl MidtransClient for Snap {}
16
17impl Transactions for Snap {
18 fn get_api_config(&self) -> &ApiConfig {
20 &self.api_config
21 }
22
23 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 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 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 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 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(¶meters)?;
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(¶meters)?;
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(¶meters)?;
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(¶meters)?;
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(¶meters);
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(¶meters);
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(¶meters);
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(¶meters);
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(¶meters)?;
445 assert!(transaction.contains_key("token"));
446 assert!(transaction.contains_key("redirect_url"));
447 Ok(())
448 }
449 }
450}