use reqwest::{Client, StatusCode};
use url::Url;
use uuid::Uuid;
use crate::error::Error;
use crate::models::electricity::{
IdentificationParameter, MaloIdentResultNegative, MaloIdentResultPositive, ReferenceId,
};
#[cfg(feature = "crypto")]
use p256::ecdsa::SigningKey;
#[derive(Clone, Debug)]
pub struct MaloIdentClient {
inner: Client,
base_url: Url,
#[cfg(feature = "crypto")]
signing_key: Option<SigningKey>,
}
impl MaloIdentClient {
pub fn new(base_url: Url, client: Client) -> Self {
Self {
inner: client,
base_url,
#[cfg(feature = "crypto")]
signing_key: None,
}
}
#[cfg(feature = "crypto")]
pub fn with_signing(mut self, key: SigningKey) -> Self {
self.signing_key = Some(key);
self
}
pub async fn request_malo_id(
&self,
transaction_id: Uuid,
creation_date_time: &str,
params: &IdentificationParameter,
initial_transaction_id: Option<Uuid>,
) -> Result<(), Error> {
let url = self.url("maloId/request/v1")?;
let canonical = self.canonical_body(params)?;
let mut req = self
.inner
.post(url.clone())
.header("transactionId", transaction_id.to_string())
.header("creationDateTime", creation_date_time)
.header("Content-Type", "application/json")
.body(canonical.clone());
req = self.sign_if_enabled(
req,
url.as_str(),
&canonical,
creation_date_time,
&transaction_id.to_string(),
)?;
if let Some(id) = initial_transaction_id {
req = req.header("initialTransactionId", id.to_string());
}
check_accepted(req.send().await?).await
}
pub async fn send_positive_response(
&self,
transaction_id: Uuid,
creation_date_time: &str,
reference_id: ReferenceId,
result: &MaloIdentResultPositive,
initial_transaction_id: Option<Uuid>,
) -> Result<(), Error> {
let mut url = self.url("maloId/dataForMarketLocationPositive/v1")?;
url.query_pairs_mut()
.append_pair("referenceId", &reference_id.to_string());
let canonical = self.canonical_body(result)?;
let mut req = self
.inner
.post(url.clone())
.header("transactionId", transaction_id.to_string())
.header("creationDateTime", creation_date_time)
.header("Content-Type", "application/json")
.body(canonical.clone());
req = self.sign_if_enabled(
req,
url.as_str(),
&canonical,
creation_date_time,
&transaction_id.to_string(),
)?;
if let Some(id) = initial_transaction_id {
req = req.header("initialTransactionId", id.to_string());
}
check_accepted(req.send().await?).await
}
pub async fn send_negative_response(
&self,
transaction_id: Uuid,
creation_date_time: &str,
reference_id: ReferenceId,
result: &MaloIdentResultNegative,
initial_transaction_id: Option<Uuid>,
) -> Result<(), Error> {
let mut url = self.url("maloId/dataForMarketLocationNegative/v1")?;
url.query_pairs_mut()
.append_pair("referenceId", &reference_id.to_string());
let canonical = self.canonical_body(result)?;
let mut req = self
.inner
.post(url.clone())
.header("transactionId", transaction_id.to_string())
.header("creationDateTime", creation_date_time)
.header("Content-Type", "application/json")
.body(canonical.clone());
req = self.sign_if_enabled(
req,
url.as_str(),
&canonical,
creation_date_time,
&transaction_id.to_string(),
)?;
if let Some(id) = initial_transaction_id {
req = req.header("initialTransactionId", id.to_string());
}
check_accepted(req.send().await?).await
}
fn url(&self, path: &str) -> Result<Url, Error> {
self.base_url.join(path).map_err(Error::Url)
}
fn canonical_body<T: serde::Serialize>(&self, value: &T) -> Result<Vec<u8>, Error> {
#[cfg(feature = "crypto")]
if self.signing_key.is_some() {
return crate::transport::content_security::canonical_json(value);
}
serde_json::to_vec(value).map_err(Error::Json)
}
#[inline]
fn sign_if_enabled(
&self,
req: reqwest::RequestBuilder,
_uri: &str,
_canonical_payload: &[u8],
_creation_dt: &str,
_tx_id: &str,
) -> Result<reqwest::RequestBuilder, Error> {
#[cfg(feature = "crypto")]
if let Some(key) = &self.signing_key {
use crate::transport::content_security::{self, HEADER_DIGEST, HEADER_SIGNATURE};
let (digest, sig) = content_security::sign_request(
_uri,
_canonical_payload,
_creation_dt,
_tx_id,
key,
)?;
return Ok(req
.header(HEADER_DIGEST, digest)
.header(HEADER_SIGNATURE, sig));
}
Ok(req)
}
}
async fn check_accepted(resp: reqwest::Response) -> Result<(), Error> {
match resp.status() {
StatusCode::ACCEPTED => Ok(()),
s => Err(Error::Http {
status: s.as_u16(),
body: resp.text().await.unwrap_or_default(),
}),
}
}