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(¶ms)
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}