swish_api/
client.rs

1//! # The SwishClient
2//!
3//! This is the client that's used to make calls to the Swish API.
4//!
5use error::{RequestError, SwishClientError};
6use futures::stream::Stream;
7use futures::{future, Future};
8use hyper::client::HttpConnector;
9use hyper::header::{self, HeaderValue, CONTENT_TYPE, LOCATION};
10use hyper::Client as HttpClient;
11use hyper::StatusCode;
12use hyper::{self, Body, Request, Uri};
13use hyper_tls::HttpsConnector;
14use native_tls::{Identity, TlsConnector};
15use serde::de::DeserializeOwned;
16use serde::Serialize;
17use serde_json;
18use std::error;
19use std::fmt;
20use std::fs::File;
21use std::io;
22use std::io::Read;
23use std::path::Path;
24use std::str;
25use tokio_core::reactor::Handle;
26
27/// The client used to make call to the Swish API.
28#[derive(Debug)]
29pub struct SwishClient {
30    merchant_swish_number: String,
31    swish_api_url: String,
32    passphrase: String,
33    cert_path: String,
34    handle: Handle,
35}
36
37/// This is what will be returned when a payment is
38/// successfully created at Swish.
39#[derive(Debug, Serialize, Deserialize)]
40pub struct CreatedPayment {
41    pub id: String,
42    pub location: String,
43    pub request_token: Option<String>,
44}
45
46/// This is all the data that's returned from the
47/// Swish API when fetching a payment.
48#[derive(Debug, Deserialize, Clone)]
49pub struct Payment {
50    pub id: String,
51    pub amount: f64,
52    #[serde(rename = "payeePaymentReference")]
53    pub payee_payment_reference: Option<String>,
54    #[serde(rename = "paymentReference")]
55    pub payment_reference: Option<String>,
56    #[serde(rename = "payerAlias")]
57    pub payer_alias: Option<String>,
58    #[serde(rename = "payeeAlias")]
59    pub payee_alias: Option<String>,
60
61    pub message: Option<String>,
62    pub status: Option<Status>,
63    #[serde(rename = "dateCreated")]
64    pub date_created: String,
65    pub currency: Currency,
66    #[serde(rename = "datePaid")]
67    pub date_paid: Option<String>,
68
69    // Errors can occur
70    #[serde(rename = "errorCode")]
71    pub error_code: Option<String>,
72    #[serde(rename = "errorMessage")]
73    pub error_message: Option<String>,
74}
75
76/// The status of an operation.
77#[derive(Debug, Deserialize, Clone, PartialEq)]
78pub enum Status {
79    #[serde(rename = "CREATED")]
80    Created,
81    #[serde(rename = "PAID")]
82    Paid,
83    #[serde(rename = "ERROR")]
84    Error,
85    #[serde(rename = "VALIDATED")]
86    Validated,
87    #[serde(rename = "INITIATED")]
88    Initiated,
89}
90
91/// Params used to create a new payment.
92#[derive(Debug, Default, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct PaymentParams<'a> {
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub payee_payment_reference: Option<&'a str>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub payer_alias: Option<&'a str>,
99    pub payee_alias: &'a str,
100
101    pub amount: f64,
102    currency: Currency,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub message: Option<&'a str>,
105    pub callback_url: &'a str,
106}
107
108/// Params used to create a new refund.
109#[derive(Debug, Default, Serialize)]
110#[serde(rename_all = "camelCase")]
111pub struct RefundParams<'a> {
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub payer_payment_reference: Option<&'a str>,
114    pub original_payment_reference: &'a str,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub payment_reference: Option<&'a str>,
117    pub payer_alias: &'a str,
118    pub payee_alias: &'a str,
119    pub amount: f64,
120    currency: Currency,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub message: Option<&'a str>,
123    pub callback_url: &'a str,
124}
125
126/// The currency the Swish API supports.
127#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
128pub enum Currency {
129    /// SEK is currently the only currency supported at Swish.
130    SEK,
131}
132
133impl Default for Currency {
134    fn default() -> Self {
135        Currency::SEK
136    }
137}
138
139/// This will be returned when a refund
140/// is successfully created.
141#[derive(Debug, Serialize, Deserialize)]
142pub struct CreatedRefund {
143    pub id: String,
144    pub location: String,
145}
146
147/// This is all the data that's returned
148/// from the Swish API when fetching a refund.
149#[derive(Debug, Deserialize, Clone)]
150pub struct Refund {
151    pub id: String,
152    pub amount: f64,
153    #[serde(rename = "payerPaymentReference")]
154    pub payer_payment_reference: Option<String>,
155    #[serde(rename = "originalpaymentReference")]
156    pub original_payment_reference: Option<String>,
157    #[serde(rename = "payerAlias")]
158    pub payer_alias: Option<String>,
159    #[serde(rename = "payeeAlias")]
160    pub payee_alias: Option<String>,
161
162    pub message: Option<String>,
163    pub status: Option<Status>,
164    #[serde(rename = "dateCreated")]
165    pub date_created: String,
166    pub currency: Currency,
167    #[serde(rename = "datePaid")]
168    pub date_paid: Option<String>,
169
170    // Errors can occur
171    #[serde(rename = "errorCode")]
172    pub error_code: Option<String>,
173    #[serde(rename = "errorMessage")]
174    pub error_message: Option<String>,
175    #[serde(rename = "additionalInformation")]
176    pub additional_information: Option<String>,
177}
178
179/// Custom Header returned by the Swish API.
180const PAYMENT_REQUEST_TOKEN: &str = "paymentrequesttoken";
181
182/// Type alias for Future used within the SwishClient
183type SwishBoxFuture<'a, T> = Box<Future<Item = T, Error = SwishClientError> + 'a>;
184
185impl SwishClient {
186    /// [`SwishClient`]: struct.SwishClient.html
187    ///
188    /// Creates a new SwishClient
189    ///
190    /// # Arguments
191    ///
192    /// * `merchant_swish_number` - The merchants swish number which will receive the payments.
193    /// * `cert_path` - The path to the certificate.
194    /// * `passphrase` - The passphrase to the certificate.
195    /// * `handle` - A tokio reactor handle.
196    ///
197    /// # Returns
198    /// A configured [`SwishClient`].
199    ///
200    /// # Example
201    ///
202    /// ```
203    /// extern crate tokio_core;
204    /// extern crate swish_api;
205    ///
206    /// use swish_api::client::SwishClient;
207    /// use tokio_core::reactor::Core;
208    /// use std::env;
209    ///
210    /// let core = Core::new().unwrap();
211    /// let handle = core.handle();
212    /// let current_dir = env::current_dir().unwrap();
213    /// let cert_path = current_dir.join("./certs/test_cert.p12");
214    /// let swish_client = cert_path
215    ///    .into_os_string()
216    ///    .to_str()
217    ///    .map(|cert_path_string| {
218    ///        SwishClient::new("1231181189", cert_path_string, "swish", handle)
219    ///    }).unwrap();
220    /// ```
221    pub fn new(
222        merchant_swish_number: &str,
223        cert_path: &str,
224        passphrase: &str,
225        handle: Handle,
226    ) -> Self {
227        SwishClient {
228            merchant_swish_number: merchant_swish_number.to_owned(),
229            swish_api_url: "https://mss.cpc.getswish.net/swish-cpcapi/api/v1/".to_owned(),
230            passphrase: passphrase.to_owned(),
231            cert_path: cert_path.to_owned(),
232            handle,
233        }
234    }
235
236    /// [`PaymentParams`]: struct.PaymentParams.html
237    /// [`CreatedPayment`]: struct.CreatedPayment.html
238    ///
239    /// Creates a payment with the provided [`PaymentParams`].
240    ///
241    /// # Returns
242    /// A Future with a [`CreatedPayment`].
243    ///
244    /// # Arguments
245    ///
246    /// * `params` - [`PaymentParams`].
247    ///
248    /// # Example
249    ///
250    /// ```
251    /// extern crate tokio_core;
252    /// extern crate swish_api;
253    ///
254    /// use tokio_core::reactor::Core;
255    /// use std::env;
256    /// use swish_api::client::{PaymentParams, SwishClient};
257    ///
258    /// let core = Core::new().unwrap();
259    /// let handle = core.handle();
260    /// let current_dir = env::current_dir().unwrap();
261    /// let cert_path = current_dir.join("./tests/test_cert.p12");
262    /// let root_cert_path = current_dir.join("./tests/root_cert.der");
263    /// let swish_client = cert_path
264    ///    .into_os_string()
265    ///    .to_str()
266    ///    .map(|cert_path_string| {
267    ///        SwishClient::new("1231181189", cert_path_string, "swish", handle)
268    ///    }).unwrap();
269    ///
270    /// let mut payment_params = PaymentParams::default();
271    /// payment_params.amount = 100.00;
272    /// payment_params.payee_alias = "1231181189";
273    /// payment_params.payee_payment_reference = Some("0123456789");
274    /// payment_params.callback_url = "https://example.com/api/swishcb/paymentrequests";
275    /// payment_params.message = Some("Kingston USB Flash Drive 8 GB");
276    ///
277    /// let payment = swish_client.create_payment(payment_params);
278    ///
279    /// ```
280    pub fn create_payment<'a>(
281        &'a self,
282        params: PaymentParams,
283    ) -> SwishBoxFuture<'a, CreatedPayment> {
284        let payment_params = PaymentParams {
285            payee_alias: self.merchant_swish_number.as_str(),
286            ..params
287        };
288
289        let response: SwishBoxFuture<'a, (String, header::HeaderMap)> =
290            self.post::<CreatedPayment, PaymentParams>("paymentrequests", payment_params);
291
292        let payment_future = response.and_then(move |(_, headers)| {
293            let location = get_header_as_string(&headers, LOCATION);
294            let request_token = get_header_as_string(
295                &headers,
296                header::HeaderName::from_static(PAYMENT_REQUEST_TOKEN),
297            );
298
299            let payment = location.and_then(|location| {
300                self.get_payment_id_from_location(&location)
301                    .map(|payment_id| CreatedPayment {
302                        id: payment_id,
303                        request_token,
304                        location,
305                    })
306            });
307
308            future::result(serde_json::from_value(json!(payment)).map_err(SwishClientError::from))
309        });
310        Box::new(payment_future)
311    }
312
313    /// [`Payment`]: struct.Payment.html
314    ///
315    /// Gets a payment for a given `payment_id`.
316    ///
317    /// # Returns
318    /// A Future with a [`Payment`].
319    ///
320    /// # Arguments
321    ///
322    /// * `payment_id` - A string id for a payment
323    ///
324    /// # Example
325    ///
326    /// ```
327    /// extern crate tokio_core;
328    /// extern crate swish_api;
329    ///
330    /// use tokio_core::reactor::Core;
331    /// use std::env;
332    /// use swish_api::client::SwishClient;
333    ///
334    /// let core = Core::new().unwrap();
335    /// let handle = core.handle();
336    /// let current_dir = env::current_dir().unwrap();
337    /// let cert_path = current_dir.join("./tests/test_cert.p12");
338    /// let root_cert_path = current_dir.join("./tests/root_cert.der");
339    /// let swish_client = cert_path
340    ///    .into_os_string()
341    ///    .to_str()
342    ///    .map(|cert_path_string| {
343    ///        SwishClient::new("1231181189", cert_path_string, "swish", handle)
344    ///    }).unwrap();
345    ///
346    /// let payment_id = "111";
347    /// let payment = swish_client.get_payment(payment_id);
348    /// ```
349    pub fn get_payment<'a>(&'a self, payment_id: &str) -> SwishBoxFuture<'a, Payment> {
350        self.get(format!("paymentrequests/{}", payment_id).as_str())
351    }
352
353    /// [`RefundParams`]: struct.RefundParams.html
354    /// [`CreatedRefund`]: struct.CreatedRefund.html
355    ///
356    /// Creates a refund with the provided [`RefundParams`].
357    ///
358    /// # Returns
359    /// A Future with a [`CreatedRefund`].
360    ///
361    /// # Arguments
362    ///
363    /// * `params` - [`RefundParams`].
364    ///
365    /// # Example
366    ///
367    /// ```
368    /// extern crate tokio_core;
369    /// extern crate swish_api;
370    ///
371    /// use tokio_core::reactor::Core;
372    /// use std::env;
373    /// use swish_api::client::{RefundParams, SwishClient};
374    ///
375    /// let core = Core::new().unwrap();
376    /// let handle = core.handle();
377    /// let current_dir = env::current_dir().unwrap();
378    /// let cert_path = current_dir.join("./tests/test_cert.p12");
379    /// let root_cert_path = current_dir.join("./tests/root_cert.der");
380    /// let swish_client = cert_path
381    ///    .into_os_string()
382    ///    .to_str()
383    ///    .map(|cert_path_string| {
384    ///        SwishClient::new("1231181189", cert_path_string, "swish", handle)
385    ///    }).unwrap();
386    ///
387    /// let mut refund_params = RefundParams::default();
388    /// refund_params.amount = 100.00;
389    /// refund_params.callback_url = "https://example.com/api/swishcb/refunds";
390    /// refund_params.payer_payment_reference = Some("0123456789");
391    /// refund_params.message = Some("Refund for Kingston USB Flash Drive 8 GB");
392    ///
393    /// let refund = swish_client.create_refund(refund_params);
394    /// ```
395    pub fn create_refund<'a>(&'a self, params: RefundParams) -> SwishBoxFuture<'a, CreatedRefund> {
396        let refund_params = RefundParams {
397            payer_alias: self.merchant_swish_number.as_str(),
398            ..params
399        };
400
401        let response = self.post::<CreatedRefund, RefundParams>("refunds", refund_params);
402
403        let refund_future = response.and_then(move |(_, headers)| {
404            let location = get_header_as_string(&headers, LOCATION);
405
406            let refund = location.and_then(|location| {
407                self.get_payment_id_from_location(&location)
408                    .map(|refund_id| CreatedRefund {
409                        id: refund_id,
410                        location,
411                    })
412            });
413
414            future::result(serde_json::from_value(json!(refund)).map_err(SwishClientError::from))
415        });
416        Box::new(refund_future)
417    }
418
419    /// [`Refund`]: struct.Refund.html
420    ///
421    /// Gets a refund for a given `refund_id`.
422    ///
423    /// # Returns
424    /// A Future with a [`Refund`].
425    ///
426    /// # Arguments
427    ///
428    /// * `refund_id` - A string id for a refund
429    ///
430    /// # Example
431    ///
432    /// ```
433    /// extern crate tokio_core;
434    /// extern crate swish_api;
435    ///
436    /// use tokio_core::reactor::Core;
437    /// use std::env;
438    /// use swish_api::client::SwishClient;
439    ///
440    /// let core = Core::new().unwrap();
441    /// let handle = core.handle();
442    /// let current_dir = env::current_dir().unwrap();
443    /// let cert_path = current_dir.join("./tests/test_cert.p12");
444    /// let root_cert_path = current_dir.join("./tests/root_cert.der");
445    /// let swish_client = cert_path
446    ///    .into_os_string()
447    ///    .to_str()
448    ///    .map(|cert_path_string| {
449    ///        SwishClient::new("1231181189", cert_path_string, "swish", handle)
450    ///    }).unwrap();
451    ///
452    /// let refund_id = "111";
453    /// let refund = swish_client.get_refund(refund_id);
454    /// ```
455    pub fn get_refund<'a>(&'a self, refund_id: &str) -> SwishBoxFuture<'a, Refund> {
456        self.get(format!("refunds/{}", refund_id).as_str())
457    }
458
459    /// Reads a given cert into a Vec.
460    /// Returns a Result that contains the Vec if it succeeded.
461    ///
462    /// # Arguments
463    ///
464    /// * `cert_path` - A string path to the place where the cert is
465    fn read_cert(&self, cert_path: &str) -> Result<Vec<u8>, io::Error> {
466        let cert_path = Path::new(&cert_path);
467        let mut buf = vec![];
468        let _result = File::open(cert_path).and_then(|mut f| f.read_to_end(&mut buf));
469        Ok(buf)
470    }
471
472    /// Build a HTTPS client with the root_cert and the client_cert.
473    /// # Returns
474    /// A Result that contains the client if it succeeded.
475    fn build_client(
476        &self,
477    ) -> Result<HttpClient<HttpsConnector<HttpConnector>, Body>, Box<error::Error>> {
478        let pkcs12_cert = &self.read_cert(&self.cert_path)?;
479        let client_cert = Identity::from_pkcs12(&pkcs12_cert, &self.passphrase)?;
480
481        let tls_connector = TlsConnector::builder().identity(client_cert).build()?;
482
483        let mut http_connector = HttpConnector::new(4);
484        http_connector.enforce_http(false);
485
486        let https_connector = HttpsConnector::from((http_connector, tls_connector));
487
488        let client = hyper::client::Client::builder().build(https_connector);
489
490        Ok(client)
491    }
492
493    /// Performs a http POST request to the Swish API.
494    ///
495    /// # Returns
496    /// A Future with a Tuple that contains the body as a String
497    /// and the Response headers.
498    ///
499    /// # Arguments
500    ///
501    /// * `path` - A string path
502    /// * `params` - Params that implements Serialize which are json sent as the body
503    fn post<'a, T: 'a, P>(
504        &'a self,
505        path: &str,
506        params: P,
507    ) -> SwishBoxFuture<'a, (String, hyper::header::HeaderMap)>
508    where
509        T: DeserializeOwned + fmt::Debug,
510        P: Serialize,
511    {
512        let future_result: Result<_, SwishClientError> = self
513            .get_uri(path)
514            .and_then(|uri| {
515                serde_json::to_string(&params)
516                    .and_then(|json_params| {
517                        let mut request = Request::post(uri.to_owned())
518                            .body(Body::from(json_params))
519                            .unwrap();
520                        request
521                            .headers_mut()
522                            .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
523
524                        Ok(self.perform_swish_api_request(request))
525                    }).map_err(SwishClientError::from)
526            }).and_then(Ok);
527        Box::new(future::result(future_result).flatten())
528    }
529
530    /// Performs a http GET request to the Swish API.
531    ///
532    /// # Returns
533    /// A Future with a Tuple that contains the body as a String
534    /// and the Response headers.
535    ///
536    /// # Arguments
537    ///
538    /// * `path` - A string path
539    fn get<'a, T: 'a>(&'a self, path: &str) -> SwishBoxFuture<'a, T>
540    where
541        T: DeserializeOwned + fmt::Debug,
542    {
543        let uri = self.get_uri(path).unwrap();
544        let request = Request::get(uri).body(Body::empty()).unwrap();
545
546        let future = self
547            .perform_swish_api_request(request)
548            .and_then(move |(body, _)| future::result(self.parse_body::<T>(&body)));
549        Box::new(future)
550    }
551
552    /// Parse body as json.
553    ///
554    /// # Arguments
555    ///
556    /// * `body` - A string body
557    fn parse_body<T>(&self, body: &str) -> Result<T, SwishClientError>
558    where
559        T: DeserializeOwned + fmt::Debug,
560    {
561        serde_json::from_str(&body).map_err(SwishClientError::from)
562    }
563
564    /// Parse a given string path into an Uri.
565    ///
566    /// # Arguments
567    ///
568    /// * `path` - A string path
569    fn get_uri(&self, path: &str) -> Result<Uri, SwishClientError> {
570        format!("{}{}", self.swish_api_url, path)
571            .parse::<Uri>()
572            .map_err(SwishClientError::from)
573    }
574
575    /// Gets a payment_id from a given location header string.
576    ///
577    /// # Arguments
578    ///
579    /// * `location` - A string location header
580    fn get_payment_id_from_location(&self, location: &str) -> Option<String> {
581        let payment_id: Vec<&str> = location.split('/').collect();
582        payment_id.last().cloned().map(|id| id.to_string())
583    }
584
585    /// Performs the actual request to the Swish API.
586    /// # Returns
587    /// A Future with a Tuple that contains the body as a String
588    /// and the Response headers.
589    fn perform_swish_api_request<'a>(
590        &'a self,
591        request: Request<hyper::Body>,
592    ) -> SwishBoxFuture<'a, (String, hyper::HeaderMap)> {
593        let client = self
594            .build_client()
595            .expect("The HttpsClient couldn't be built, the certificate is probably wrong");
596
597        let future = client
598            .request(request)
599            .map_err(SwishClientError::from)
600            .and_then(move |response| {
601                let status = response.status();
602                let headers = response.headers().to_owned();
603
604                response
605                    .into_body()
606                    .concat2()
607                    .map_err(SwishClientError::from)
608                    .and_then(move |body| {
609                        let body = str::from_utf8(&body).unwrap();
610
611                        if status == StatusCode::NOT_FOUND {
612                            let error = RequestError {
613                                http_status: StatusCode::NOT_FOUND,
614                                code: None,
615                                additional_information: None,
616                                message: body.to_owned(),
617                            };
618                            return future::err(SwishClientError::from(error));
619                        }
620
621                        if !status.is_success() {
622                            // Swish can sometimes return an array of errors.
623                            // TODO: http_status is wrong, set it to the actually status.
624                            let errors: SwishClientError =
625                                match serde_json::from_str::<serde_json::Value>(body) {
626                                    Ok(json) => {
627                                        let errors: Vec<_> = json
628                                            .as_array()
629                                            .into_iter()
630                                            .flat_map(|e| {
631                                                e.iter()
632                                                    .flat_map(|err| {
633                                                        serde_json::from_value::<RequestError>(
634                                                            err.clone(),
635                                                        )
636                                                    }).map(|request_error| RequestError {
637                                                        http_status: status,
638                                                        ..request_error
639                                                    }).map(SwishClientError::from)
640                                                    .collect::<Vec<SwishClientError>>()
641                                            }).collect();
642                                        SwishClientError::from(errors)
643                                    }
644                                    Err(err) => {
645                                        let error = RequestError {
646                                            additional_information: None,
647                                            code: None,
648                                            http_status: status,
649                                            message: err.to_string(),
650                                        };
651                                        SwishClientError::from(error)
652                                    }
653                                };
654                            return future::err(errors);
655                        }
656                        future::result(Ok((body.to_owned(), headers)))
657                    })
658            });
659        Box::new(future)
660    }
661}
662
663/// Gets a hyper::Header and turns it into a String.
664///
665/// # Arguments
666///
667/// * `headers` - hyper::Headers
668fn get_header_as_string(
669    headers: &hyper::header::HeaderMap,
670    header: hyper::header::HeaderName,
671) -> Option<String> {
672    headers
673        .get(header)
674        .and_then(|h| h.to_str().ok().map(|h| h.to_string()))
675}