use error::{RequestError, SwishClientError};
use futures::stream::Stream;
use futures::{future, Future};
use hyper::client::HttpConnector;
use hyper::header::{self, HeaderValue, CONTENT_TYPE, LOCATION};
use hyper::Client as HttpClient;
use hyper::StatusCode;
use hyper::{self, Body, Request, Uri};
use hyper_tls::HttpsConnector;
use native_tls::{Identity, TlsConnector};
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json;
use std::error;
use std::fmt;
use std::fs::File;
use std::io;
use std::io::Read;
use std::path::Path;
use std::str;
use tokio_core::reactor::Handle;
#[derive(Debug)]
pub struct SwishClient {
merchant_swish_number: String,
swish_api_url: String,
passphrase: String,
cert_path: String,
handle: Handle,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreatedPayment {
pub id: String,
pub location: String,
pub request_token: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Payment {
pub id: String,
pub amount: f64,
#[serde(rename = "payeePaymentReference")]
pub payee_payment_reference: Option<String>,
#[serde(rename = "paymentReference")]
pub payment_reference: Option<String>,
#[serde(rename = "payerAlias")]
pub payer_alias: Option<String>,
#[serde(rename = "payeeAlias")]
pub payee_alias: Option<String>,
pub message: Option<String>,
pub status: Option<Status>,
#[serde(rename = "dateCreated")]
pub date_created: String,
pub currency: Currency,
#[serde(rename = "datePaid")]
pub date_paid: Option<String>,
#[serde(rename = "errorCode")]
pub error_code: Option<String>,
#[serde(rename = "errorMessage")]
pub error_message: Option<String>,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Status {
#[serde(rename = "CREATED")]
Created,
#[serde(rename = "PAID")]
Paid,
#[serde(rename = "ERROR")]
Error,
#[serde(rename = "VALIDATED")]
Validated,
#[serde(rename = "INITIATED")]
Initiated,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentParams<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub payee_payment_reference: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payer_alias: Option<&'a str>,
pub payee_alias: &'a str,
pub amount: f64,
currency: Currency,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<&'a str>,
pub callback_url: &'a str,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RefundParams<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub payer_payment_reference: Option<&'a str>,
pub original_payment_reference: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_reference: Option<&'a str>,
pub payer_alias: &'a str,
pub payee_alias: &'a str,
pub amount: f64,
currency: Currency,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<&'a str>,
pub callback_url: &'a str,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum Currency {
SEK,
}
impl Default for Currency {
fn default() -> Self {
Currency::SEK
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreatedRefund {
pub id: String,
pub location: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Refund {
pub id: String,
pub amount: f64,
#[serde(rename = "payerPaymentReference")]
pub payer_payment_reference: Option<String>,
#[serde(rename = "originalpaymentReference")]
pub original_payment_reference: Option<String>,
#[serde(rename = "payerAlias")]
pub payer_alias: Option<String>,
#[serde(rename = "payeeAlias")]
pub payee_alias: Option<String>,
pub message: Option<String>,
pub status: Option<Status>,
#[serde(rename = "dateCreated")]
pub date_created: String,
pub currency: Currency,
#[serde(rename = "datePaid")]
pub date_paid: Option<String>,
#[serde(rename = "errorCode")]
pub error_code: Option<String>,
#[serde(rename = "errorMessage")]
pub error_message: Option<String>,
#[serde(rename = "additionalInformation")]
pub additional_information: Option<String>,
}
const PAYMENT_REQUEST_TOKEN: &str = "paymentrequesttoken";
type SwishBoxFuture<'a, T> = Box<Future<Item = T, Error = SwishClientError> + 'a>;
impl SwishClient {
pub fn new(
merchant_swish_number: &str,
cert_path: &str,
passphrase: &str,
handle: Handle,
) -> Self {
SwishClient {
merchant_swish_number: merchant_swish_number.to_owned(),
swish_api_url: "https://mss.cpc.getswish.net/swish-cpcapi/api/v1/".to_owned(),
passphrase: passphrase.to_owned(),
cert_path: cert_path.to_owned(),
handle,
}
}
pub fn create_payment<'a>(
&'a self,
params: PaymentParams,
) -> SwishBoxFuture<'a, CreatedPayment> {
let payment_params = PaymentParams {
payee_alias: self.merchant_swish_number.as_str(),
..params
};
let response: SwishBoxFuture<'a, (String, header::HeaderMap)> =
self.post::<CreatedPayment, PaymentParams>("paymentrequests", payment_params);
let payment_future = response.and_then(move |(_, headers)| {
let location = get_header_as_string(&headers, LOCATION);
let request_token = get_header_as_string(
&headers,
header::HeaderName::from_static(PAYMENT_REQUEST_TOKEN),
);
let payment = location.and_then(|location| {
self.get_payment_id_from_location(&location)
.map(|payment_id| CreatedPayment {
id: payment_id,
request_token,
location,
})
});
future::result(serde_json::from_value(json!(payment)).map_err(SwishClientError::from))
});
Box::new(payment_future)
}
pub fn get_payment<'a>(&'a self, payment_id: &str) -> SwishBoxFuture<'a, Payment> {
self.get(format!("paymentrequests/{}", payment_id).as_str())
}
pub fn create_refund<'a>(&'a self, params: RefundParams) -> SwishBoxFuture<'a, CreatedRefund> {
let refund_params = RefundParams {
payer_alias: self.merchant_swish_number.as_str(),
..params
};
let response = self.post::<CreatedRefund, RefundParams>("refunds", refund_params);
let refund_future = response.and_then(move |(_, headers)| {
let location = get_header_as_string(&headers, LOCATION);
let refund = location.and_then(|location| {
self.get_payment_id_from_location(&location)
.map(|refund_id| CreatedRefund {
id: refund_id,
location,
})
});
future::result(serde_json::from_value(json!(refund)).map_err(SwishClientError::from))
});
Box::new(refund_future)
}
pub fn get_refund<'a>(&'a self, refund_id: &str) -> SwishBoxFuture<'a, Refund> {
self.get(format!("refunds/{}", refund_id).as_str())
}
fn read_cert(&self, cert_path: &str) -> Result<Vec<u8>, io::Error> {
let cert_path = Path::new(&cert_path);
let mut buf = vec![];
let _result = File::open(cert_path).and_then(|mut f| f.read_to_end(&mut buf));
Ok(buf)
}
fn build_client(
&self,
) -> Result<HttpClient<HttpsConnector<HttpConnector>, Body>, Box<error::Error>> {
let pkcs12_cert = &self.read_cert(&self.cert_path)?;
let client_cert = Identity::from_pkcs12(&pkcs12_cert, &self.passphrase)?;
let tls_connector = TlsConnector::builder().identity(client_cert).build()?;
let mut http_connector = HttpConnector::new(4);
http_connector.enforce_http(false);
let https_connector = HttpsConnector::from((http_connector, tls_connector));
let client = hyper::client::Client::builder().build(https_connector);
Ok(client)
}
fn post<'a, T: 'a, P>(
&'a self,
path: &str,
params: P,
) -> SwishBoxFuture<'a, (String, hyper::header::HeaderMap)>
where
T: DeserializeOwned + fmt::Debug,
P: Serialize,
{
let future_result: Result<_, SwishClientError> = self
.get_uri(path)
.and_then(|uri| {
serde_json::to_string(¶ms)
.and_then(|json_params| {
let mut request = Request::post(uri.to_owned())
.body(Body::from(json_params))
.unwrap();
request
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
Ok(self.perform_swish_api_request(request))
}).map_err(SwishClientError::from)
}).and_then(Ok);
Box::new(future::result(future_result).flatten())
}
fn get<'a, T: 'a>(&'a self, path: &str) -> SwishBoxFuture<'a, T>
where
T: DeserializeOwned + fmt::Debug,
{
let uri = self.get_uri(path).unwrap();
let request = Request::get(uri).body(Body::empty()).unwrap();
let future = self
.perform_swish_api_request(request)
.and_then(move |(body, _)| future::result(self.parse_body::<T>(&body)));
Box::new(future)
}
fn parse_body<T>(&self, body: &str) -> Result<T, SwishClientError>
where
T: DeserializeOwned + fmt::Debug,
{
serde_json::from_str(&body).map_err(SwishClientError::from)
}
fn get_uri(&self, path: &str) -> Result<Uri, SwishClientError> {
format!("{}{}", self.swish_api_url, path)
.parse::<Uri>()
.map_err(SwishClientError::from)
}
fn get_payment_id_from_location(&self, location: &str) -> Option<String> {
let payment_id: Vec<&str> = location.split('/').collect();
payment_id.last().cloned().map(|id| id.to_string())
}
fn perform_swish_api_request<'a>(
&'a self,
request: Request<hyper::Body>,
) -> SwishBoxFuture<'a, (String, hyper::HeaderMap)> {
let client = self
.build_client()
.expect("The HttpsClient couldn't be built, the certificate is probably wrong");
let future = client
.request(request)
.map_err(SwishClientError::from)
.and_then(move |response| {
let status = response.status();
let headers = response.headers().to_owned();
response
.into_body()
.concat2()
.map_err(SwishClientError::from)
.and_then(move |body| {
let body = str::from_utf8(&body).unwrap();
if status == StatusCode::NOT_FOUND {
let error = RequestError {
http_status: StatusCode::NOT_FOUND,
code: None,
additional_information: None,
message: body.to_owned(),
};
return future::err(SwishClientError::from(error));
}
if !status.is_success() {
let errors: SwishClientError =
match serde_json::from_str::<serde_json::Value>(body) {
Ok(json) => {
let errors: Vec<_> = json
.as_array()
.into_iter()
.flat_map(|e| {
e.iter()
.flat_map(|err| {
serde_json::from_value::<RequestError>(
err.clone(),
)
}).map(|request_error| RequestError {
http_status: status,
..request_error
}).map(SwishClientError::from)
.collect::<Vec<SwishClientError>>()
}).collect();
SwishClientError::from(errors)
}
Err(err) => {
let error = RequestError {
additional_information: None,
code: None,
http_status: status,
message: err.to_string(),
};
SwishClientError::from(error)
}
};
return future::err(errors);
}
future::result(Ok((body.to_owned(), headers)))
})
});
Box::new(future)
}
}
fn get_header_as_string(
headers: &hyper::header::HeaderMap,
header: hyper::header::HeaderName,
) -> Option<String> {
headers
.get(header)
.and_then(|h| h.to_str().ok().map(|h| h.to_string()))
}