mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
feat(connector_integration): integrate Rapyd connector (#357)
This commit is contained in:
@ -82,6 +82,9 @@ base_url = "https://apitest.cybersource.com/"
|
|||||||
[connectors.shift4]
|
[connectors.shift4]
|
||||||
base_url = "https://api.shift4.com/"
|
base_url = "https://api.shift4.com/"
|
||||||
|
|
||||||
|
[connectors.rapyd]
|
||||||
|
base_url = "https://sandboxapi.rapyd.net"
|
||||||
|
|
||||||
[connectors.fiserv]
|
[connectors.fiserv]
|
||||||
base_url = "https://cert.api.fiservapps.com/"
|
base_url = "https://cert.api.fiservapps.com/"
|
||||||
|
|
||||||
@ -90,6 +93,7 @@ base_url = "http://localhost:9090/"
|
|||||||
|
|
||||||
[connectors.payu]
|
[connectors.payu]
|
||||||
base_url = "https://secure.snd.payu.com/api/"
|
base_url = "https://secure.snd.payu.com/api/"
|
||||||
|
|
||||||
[connectors.globalpay]
|
[connectors.globalpay]
|
||||||
base_url = "https://apis.sandbox.globalpay.com/ucp/"
|
base_url = "https://apis.sandbox.globalpay.com/ucp/"
|
||||||
|
|
||||||
|
|||||||
@ -133,6 +133,9 @@ base_url = "https://apitest.cybersource.com/"
|
|||||||
[connectors.shift4]
|
[connectors.shift4]
|
||||||
base_url = "https://api.shift4.com/"
|
base_url = "https://api.shift4.com/"
|
||||||
|
|
||||||
|
[connectors.rapyd]
|
||||||
|
base_url = "https://sandboxapi.rapyd.net"
|
||||||
|
|
||||||
[connectors.fiserv]
|
[connectors.fiserv]
|
||||||
base_url = "https://cert.api.fiservapps.com/"
|
base_url = "https://cert.api.fiservapps.com/"
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,9 @@ base_url = "https://apitest.cybersource.com/"
|
|||||||
[connectors.shift4]
|
[connectors.shift4]
|
||||||
base_url = "https://api.shift4.com/"
|
base_url = "https://api.shift4.com/"
|
||||||
|
|
||||||
|
[connectors.rapyd]
|
||||||
|
base_url = "https://sandboxapi.rapyd.net"
|
||||||
|
|
||||||
[connectors.fiserv]
|
[connectors.fiserv]
|
||||||
base_url = "https://cert.api.fiservapps.com/"
|
base_url = "https://cert.api.fiservapps.com/"
|
||||||
|
|
||||||
|
|||||||
@ -507,6 +507,7 @@ pub enum Connector {
|
|||||||
Globalpay,
|
Globalpay,
|
||||||
Klarna,
|
Klarna,
|
||||||
Payu,
|
Payu,
|
||||||
|
Rapyd,
|
||||||
Shift4,
|
Shift4,
|
||||||
Stripe,
|
Stripe,
|
||||||
Worldline,
|
Worldline,
|
||||||
|
|||||||
@ -63,4 +63,4 @@ max_read_count = 100
|
|||||||
|
|
||||||
[connectors.supported]
|
[connectors.supported]
|
||||||
wallets = ["klarna","braintree"]
|
wallets = ["klarna","braintree"]
|
||||||
cards = ["stripe","adyen","authorizedotnet","checkout","braintree", "cybersource", "fiserv"]
|
cards = ["stripe","adyen","authorizedotnet","checkout","braintree", "cybersource", "fiserv", "rapyd"]
|
||||||
|
|||||||
@ -131,6 +131,7 @@ pub struct Connectors {
|
|||||||
pub globalpay: ConnectorParams,
|
pub globalpay: ConnectorParams,
|
||||||
pub klarna: ConnectorParams,
|
pub klarna: ConnectorParams,
|
||||||
pub payu: ConnectorParams,
|
pub payu: ConnectorParams,
|
||||||
|
pub rapyd: ConnectorParams,
|
||||||
pub shift4: ConnectorParams,
|
pub shift4: ConnectorParams,
|
||||||
pub stripe: ConnectorParams,
|
pub stripe: ConnectorParams,
|
||||||
pub supported: SupportedConnectors,
|
pub supported: SupportedConnectors,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ pub mod fiserv;
|
|||||||
pub mod globalpay;
|
pub mod globalpay;
|
||||||
pub mod klarna;
|
pub mod klarna;
|
||||||
pub mod payu;
|
pub mod payu;
|
||||||
|
pub mod rapyd;
|
||||||
pub mod shift4;
|
pub mod shift4;
|
||||||
pub mod stripe;
|
pub mod stripe;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
@ -18,6 +19,6 @@ pub mod worldpay;
|
|||||||
pub use self::{
|
pub use self::{
|
||||||
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
||||||
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, fiserv::Fiserv,
|
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, fiserv::Fiserv,
|
||||||
globalpay::Globalpay, klarna::Klarna, payu::Payu, shift4::Shift4, stripe::Stripe,
|
globalpay::Globalpay, klarna::Klarna, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe,
|
||||||
worldline::Worldline, worldpay::Worldpay,
|
worldline::Worldline, worldpay::Worldpay,
|
||||||
};
|
};
|
||||||
|
|||||||
656
crates/router/src/connector/rapyd.rs
Normal file
656
crates/router/src/connector/rapyd.rs
Normal file
@ -0,0 +1,656 @@
|
|||||||
|
mod transformers;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use common_utils::date_time;
|
||||||
|
use error_stack::{IntoReport, ResultExt};
|
||||||
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
|
use ring::hmac;
|
||||||
|
use transformers as rapyd;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
configs::settings,
|
||||||
|
consts,
|
||||||
|
core::{
|
||||||
|
errors::{self, CustomResult},
|
||||||
|
payments,
|
||||||
|
},
|
||||||
|
headers, logger, services,
|
||||||
|
types::{
|
||||||
|
self,
|
||||||
|
api::{self, ConnectorCommon},
|
||||||
|
ErrorResponse, Response,
|
||||||
|
},
|
||||||
|
utils::{self, BytesExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Rapyd;
|
||||||
|
|
||||||
|
impl Rapyd {
|
||||||
|
pub fn generate_signature(
|
||||||
|
&self,
|
||||||
|
auth: &rapyd::RapydAuthType,
|
||||||
|
http_method: &str,
|
||||||
|
url_path: &str,
|
||||||
|
body: &str,
|
||||||
|
timestamp: &i64,
|
||||||
|
salt: &str,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
let rapyd::RapydAuthType {
|
||||||
|
access_key,
|
||||||
|
secret_key,
|
||||||
|
} = auth;
|
||||||
|
let to_sign =
|
||||||
|
format!("{http_method}{url_path}{salt}{timestamp}{access_key}{secret_key}{body}");
|
||||||
|
let key = hmac::Key::new(hmac::HMAC_SHA256, secret_key.as_bytes());
|
||||||
|
let tag = hmac::sign(&key, to_sign.as_bytes());
|
||||||
|
let hmac_sign = hex::encode(tag);
|
||||||
|
let signature_value = consts::BASE64_ENGINE_URL_SAFE.encode(hmac_sign);
|
||||||
|
Ok(signature_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectorCommon for Rapyd {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"rapyd"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_get_content_type(&self) -> &'static str {
|
||||||
|
"application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
|
||||||
|
connectors.rapyd.base_url.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_auth_header(
|
||||||
|
&self,
|
||||||
|
_auth_type: &types::ConnectorAuthType,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentAuthorize for Rapyd {}
|
||||||
|
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Authorize,
|
||||||
|
types::PaymentsAuthorizeData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Rapyd
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsAuthorizeRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
Ok(vec![(
|
||||||
|
headers::CONTENT_TYPE.to_string(),
|
||||||
|
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
self.common_get_content_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsAuthorizeRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!("{}/v1/payments", self.base_url(connectors)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::RouterData<
|
||||||
|
api::Authorize,
|
||||||
|
types::PaymentsAuthorizeData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
>,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
let timestamp = date_time::now_unix_timestamp();
|
||||||
|
let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
|
||||||
|
|
||||||
|
let rapyd_req = utils::Encode::<rapyd::RapydPaymentsRequest>::convert_and_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
|
||||||
|
let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?;
|
||||||
|
let signature =
|
||||||
|
self.generate_signature(&auth, "post", "/v1/payments", &rapyd_req, ×tamp, &salt)?;
|
||||||
|
let headers = vec![
|
||||||
|
("access_key".to_string(), auth.access_key),
|
||||||
|
("salt".to_string(), salt),
|
||||||
|
("timestamp".to_string(), timestamp.to_string()),
|
||||||
|
("signature".to_string(), signature),
|
||||||
|
];
|
||||||
|
let request = services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.url(&types::PaymentsAuthorizeType::get_url(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.headers(types::PaymentsAuthorizeType::get_headers(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.headers(headers)
|
||||||
|
.body(Some(rapyd_req))
|
||||||
|
.build();
|
||||||
|
Ok(Some(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsAuthorizeRouterData,
|
||||||
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
|
let rapyd_req = utils::Encode::<rapyd::RapydPaymentsRequest>::convert_and_url_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Some(rapyd_req))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsAuthorizeRouterData,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("Rapyd PaymentResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
logger::debug!(rapydpayments_create_response=?response);
|
||||||
|
types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Bytes,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.parse_struct("Rapyd ErrorResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
Ok(ErrorResponse {
|
||||||
|
code: response.status.error_code,
|
||||||
|
message: response.status.status,
|
||||||
|
reason: response.status.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::Payment for Rapyd {}
|
||||||
|
|
||||||
|
impl api::PreVerify for Rapyd {}
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Verify,
|
||||||
|
types::VerifyRequestData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Rapyd
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentVoid for Rapyd {}
|
||||||
|
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Void,
|
||||||
|
types::PaymentsCancelData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Rapyd
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsCancelRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
Ok(vec![(
|
||||||
|
headers::CONTENT_TYPE.to_string(),
|
||||||
|
types::PaymentsVoidType::get_content_type(self).to_string(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
self.common_get_content_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCancelRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{}/v1/payments/{}",
|
||||||
|
self.base_url(connectors),
|
||||||
|
req.request.connector_transaction_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCancelRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
let timestamp = date_time::now_unix_timestamp();
|
||||||
|
let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
|
||||||
|
|
||||||
|
let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?;
|
||||||
|
let url_path = format!("/v1/payments/{}", req.request.connector_transaction_id);
|
||||||
|
let signature =
|
||||||
|
self.generate_signature(&auth, "delete", &url_path, "", ×tamp, &salt)?;
|
||||||
|
|
||||||
|
let headers = vec![
|
||||||
|
("access_key".to_string(), auth.access_key),
|
||||||
|
("salt".to_string(), salt),
|
||||||
|
("timestamp".to_string(), timestamp.to_string()),
|
||||||
|
("signature".to_string(), signature),
|
||||||
|
];
|
||||||
|
let request = services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Delete)
|
||||||
|
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
|
||||||
|
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
|
||||||
|
.headers(headers)
|
||||||
|
.build();
|
||||||
|
Ok(Some(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsCancelRouterData,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("Rapyd PaymentResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
logger::debug!(rapydpayments_create_response=?response);
|
||||||
|
types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Bytes,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.parse_struct("Rapyd ErrorResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
Ok(ErrorResponse {
|
||||||
|
code: response.status.error_code,
|
||||||
|
message: response.status.status,
|
||||||
|
reason: response.status.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentSync for Rapyd {}
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||||
|
for Rapyd
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsSyncRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
self.common_get_content_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsSyncRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsSyncRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
_res: Bytes,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
_data: &types::PaymentsSyncRouterData,
|
||||||
|
_res: Response,
|
||||||
|
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentCapture for Rapyd {}
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Capture,
|
||||||
|
types::PaymentsCaptureData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Rapyd
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsCaptureRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
Ok(vec![(
|
||||||
|
headers::CONTENT_TYPE.to_string(),
|
||||||
|
types::PaymentsCaptureType::get_content_type(self).to_string(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
self.common_get_content_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCaptureRouterData,
|
||||||
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
|
let rapyd_req = utils::Encode::<rapyd::CaptureRequest>::convert_and_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Some(rapyd_req))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCaptureRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
let timestamp = date_time::now_unix_timestamp();
|
||||||
|
let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
|
||||||
|
|
||||||
|
let rapyd_req = utils::Encode::<rapyd::CaptureRequest>::convert_and_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
|
||||||
|
let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?;
|
||||||
|
let url_path = format!(
|
||||||
|
"/v1/payments/{}/capture",
|
||||||
|
req.request.connector_transaction_id
|
||||||
|
);
|
||||||
|
let signature =
|
||||||
|
self.generate_signature(&auth, "post", &url_path, &rapyd_req, ×tamp, &salt)?;
|
||||||
|
let headers = vec![
|
||||||
|
("access_key".to_string(), auth.access_key),
|
||||||
|
("salt".to_string(), salt),
|
||||||
|
("timestamp".to_string(), timestamp.to_string()),
|
||||||
|
("signature".to_string(), signature),
|
||||||
|
];
|
||||||
|
let request = services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
|
||||||
|
.headers(types::PaymentsCaptureType::get_headers(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.headers(headers)
|
||||||
|
.body(Some(rapyd_req))
|
||||||
|
.build();
|
||||||
|
Ok(Some(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsCaptureRouterData,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("RapydPaymentResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
|
||||||
|
types::RouterData::try_from(types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
})
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCaptureRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{}/v1/payments/{}/capture",
|
||||||
|
self.base_url(connectors),
|
||||||
|
req.request.connector_transaction_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Bytes,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.parse_struct("Rapyd ErrorResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
Ok(ErrorResponse {
|
||||||
|
code: response.status.error_code,
|
||||||
|
message: response.status.status,
|
||||||
|
reason: response.status.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentSession for Rapyd {}
|
||||||
|
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Session,
|
||||||
|
types::PaymentsSessionData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Rapyd
|
||||||
|
{
|
||||||
|
//TODO: implement sessions flow
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::Refund for Rapyd {}
|
||||||
|
impl api::RefundExecute for Rapyd {}
|
||||||
|
impl api::RefundSync for Rapyd {}
|
||||||
|
|
||||||
|
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
|
||||||
|
for Rapyd
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
_req: &types::RefundsRouterData<api::Execute>,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
Ok(vec![(
|
||||||
|
headers::CONTENT_TYPE.to_string(),
|
||||||
|
types::RefundExecuteType::get_content_type(self).to_string(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
api::ConnectorCommon::common_get_content_type(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
_req: &types::RefundsRouterData<api::Execute>,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!("{}/v1/refunds", self.base_url(connectors)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::RefundsRouterData<api::Execute>,
|
||||||
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
|
let rapyd_req = utils::Encode::<rapyd::RapydRefundRequest>::convert_and_url_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Some(rapyd_req))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::RefundsRouterData<api::Execute>,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
let timestamp = date_time::now_unix_timestamp();
|
||||||
|
let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
|
||||||
|
|
||||||
|
let rapyd_req = utils::Encode::<rapyd::RapydRefundRequest>::convert_and_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
|
||||||
|
let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?;
|
||||||
|
let signature =
|
||||||
|
self.generate_signature(&auth, "post", "/v1/refunds", &rapyd_req, ×tamp, &salt)?;
|
||||||
|
let headers = vec![
|
||||||
|
("access_key".to_string(), auth.access_key),
|
||||||
|
("salt".to_string(), salt),
|
||||||
|
("timestamp".to_string(), timestamp.to_string()),
|
||||||
|
("signature".to_string(), signature),
|
||||||
|
];
|
||||||
|
let request = services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
|
||||||
|
.headers(headers)
|
||||||
|
.body(Some(rapyd_req))
|
||||||
|
.build();
|
||||||
|
Ok(Some(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::RefundsRouterData<api::Execute>,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
|
||||||
|
logger::debug!(target: "router::connector::rapyd", response=?res);
|
||||||
|
let response: rapyd::RefundResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("rapyd RefundResponse")
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Bytes,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.parse_struct("Rapyd ErrorResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
Ok(ErrorResponse {
|
||||||
|
code: response.status.error_code,
|
||||||
|
message: response.status.status,
|
||||||
|
reason: response.status.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
|
||||||
|
for Rapyd
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
_req: &types::RefundSyncRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
self.common_get_content_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
_req: &types::RefundSyncRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("RSync".to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::RefundSyncRouterData,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||||
|
logger::debug!(target: "router::connector::rapyd", response=?res);
|
||||||
|
let response: rapyd::RefundResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("rapyd RefundResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
_res: Bytes,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("RSync".to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl api::IncomingWebhook for Rapyd {
|
||||||
|
fn get_webhook_object_reference_id(
|
||||||
|
&self,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_event_type(
|
||||||
|
&self,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_resource_object(
|
||||||
|
&self,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl services::ConnectorRedirectResponse for Rapyd {
|
||||||
|
fn get_flow_type(
|
||||||
|
&self,
|
||||||
|
_query_params: &str,
|
||||||
|
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
|
||||||
|
Ok(payments::CallConnectorAction::Trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
481
crates/router/src/connector/rapyd/transformers.rs
Normal file
481
crates/router/src/connector/rapyd/transformers.rs
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
use error_stack::{IntoReport, ResultExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::errors,
|
||||||
|
pii::{self, Secret},
|
||||||
|
services,
|
||||||
|
types::{
|
||||||
|
self, api,
|
||||||
|
storage::enums,
|
||||||
|
transformers::{self, ForeignFrom},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize)]
|
||||||
|
pub struct RapydPaymentsRequest {
|
||||||
|
pub amount: i64,
|
||||||
|
pub currency: enums::Currency,
|
||||||
|
pub payment_method: PaymentMethod,
|
||||||
|
pub payment_method_options: PaymentMethodOptions,
|
||||||
|
pub capture: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize)]
|
||||||
|
pub struct PaymentMethodOptions {
|
||||||
|
#[serde(rename = "3d_required")]
|
||||||
|
pub three_ds: bool,
|
||||||
|
}
|
||||||
|
#[derive(Default, Debug, Serialize)]
|
||||||
|
pub struct PaymentMethod {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub pm_type: String,
|
||||||
|
pub fields: PaymentFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize)]
|
||||||
|
pub struct PaymentFields {
|
||||||
|
pub number: Secret<String, pii::CardNumber>,
|
||||||
|
pub expiration_month: Secret<String>,
|
||||||
|
pub expiration_year: Secret<String>,
|
||||||
|
pub name: Secret<String>,
|
||||||
|
pub cvv: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::PaymentsAuthorizeRouterData> for RapydPaymentsRequest {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||||
|
match item.request.payment_method_data {
|
||||||
|
api_models::payments::PaymentMethod::Card(ref ccard) => {
|
||||||
|
let payment_method = PaymentMethod {
|
||||||
|
pm_type: "in_amex_card".to_owned(), //[#369] Map payment method type based on country
|
||||||
|
fields: PaymentFields {
|
||||||
|
number: ccard.card_number.to_owned(),
|
||||||
|
expiration_month: ccard.card_exp_month.to_owned(),
|
||||||
|
expiration_year: ccard.card_exp_year.to_owned(),
|
||||||
|
name: ccard.card_holder_name.to_owned(),
|
||||||
|
cvv: ccard.card_cvc.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let three_ds_enabled = matches!(item.auth_type, enums::AuthenticationType::ThreeDs);
|
||||||
|
let payment_method_options = PaymentMethodOptions {
|
||||||
|
three_ds: three_ds_enabled,
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
amount: item.request.amount,
|
||||||
|
currency: item.request.currency,
|
||||||
|
payment_method,
|
||||||
|
capture: matches!(
|
||||||
|
item.request.capture_method,
|
||||||
|
Some(enums::CaptureMethod::Automatic) | None
|
||||||
|
),
|
||||||
|
payment_method_options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RapydAuthType {
|
||||||
|
pub access_key: String,
|
||||||
|
pub secret_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::ConnectorAuthType> for RapydAuthType {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||||
|
if let types::ConnectorAuthType::BodyKey { api_key, key1 } = auth_type {
|
||||||
|
Ok(Self {
|
||||||
|
access_key: api_key.to_string(),
|
||||||
|
secret_key: key1.to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(errors::ConnectorError::FailedToObtainAuthType)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub enum RapydPaymentStatus {
|
||||||
|
#[serde(rename = "ACT")]
|
||||||
|
Active,
|
||||||
|
#[serde(rename = "CAN")]
|
||||||
|
CanceledByClientOrBank,
|
||||||
|
#[serde(rename = "CLO")]
|
||||||
|
Closed,
|
||||||
|
#[serde(rename = "ERR")]
|
||||||
|
Error,
|
||||||
|
#[serde(rename = "EXP")]
|
||||||
|
Expired,
|
||||||
|
#[serde(rename = "REV")]
|
||||||
|
ReversedByRapyd,
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "NEW")]
|
||||||
|
New,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<transformers::Foreign<(RapydPaymentStatus, String)>>
|
||||||
|
for transformers::Foreign<enums::AttemptStatus>
|
||||||
|
{
|
||||||
|
fn from(item: transformers::Foreign<(RapydPaymentStatus, String)>) -> Self {
|
||||||
|
let (status, next_action) = item.0;
|
||||||
|
match status {
|
||||||
|
RapydPaymentStatus::Closed => enums::AttemptStatus::Charged,
|
||||||
|
RapydPaymentStatus::Active => {
|
||||||
|
if next_action == "3d_verification" {
|
||||||
|
enums::AttemptStatus::AuthenticationPending
|
||||||
|
} else if next_action == "pending_capture" {
|
||||||
|
enums::AttemptStatus::Authorized
|
||||||
|
} else {
|
||||||
|
enums::AttemptStatus::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RapydPaymentStatus::CanceledByClientOrBank
|
||||||
|
| RapydPaymentStatus::Error
|
||||||
|
| RapydPaymentStatus::Expired
|
||||||
|
| RapydPaymentStatus::ReversedByRapyd => enums::AttemptStatus::Failure,
|
||||||
|
RapydPaymentStatus::New => enums::AttemptStatus::Authorizing,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct RapydPaymentsResponse {
|
||||||
|
pub status: Status,
|
||||||
|
pub data: Option<ResponseData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct Status {
|
||||||
|
pub error_code: String,
|
||||||
|
pub status: String,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub response_code: Option<String>,
|
||||||
|
pub operation_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ResponseData {
|
||||||
|
pub id: String,
|
||||||
|
pub amount: i64,
|
||||||
|
pub status: RapydPaymentStatus,
|
||||||
|
pub next_action: String,
|
||||||
|
pub redirect_url: Option<String>,
|
||||||
|
pub original_amount: Option<i64>,
|
||||||
|
pub is_partial: Option<bool>,
|
||||||
|
pub currency_code: Option<enums::Currency>,
|
||||||
|
pub country_code: Option<String>,
|
||||||
|
pub captured: Option<bool>,
|
||||||
|
pub transaction_id: String,
|
||||||
|
pub paid: Option<bool>,
|
||||||
|
pub failure_code: Option<String>,
|
||||||
|
pub failure_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<types::PaymentsResponseRouterData<RapydPaymentsResponse>>
|
||||||
|
for types::PaymentsAuthorizeRouterData
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ParsingError>;
|
||||||
|
fn try_from(
|
||||||
|
item: types::PaymentsResponseRouterData<RapydPaymentsResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let (status, response) = match item.response.status.status.as_str() {
|
||||||
|
"SUCCESS" => match item.response.data {
|
||||||
|
Some(data) => {
|
||||||
|
let redirection_data = match (data.next_action.as_str(), data.redirect_url) {
|
||||||
|
("3d_verification", Some(url)) => {
|
||||||
|
let url = Url::parse(&url)
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ParsingError)?;
|
||||||
|
let mut base_url = url.clone();
|
||||||
|
base_url.set_query(None);
|
||||||
|
Some(services::RedirectForm {
|
||||||
|
url: base_url.to_string(),
|
||||||
|
method: services::Method::Get,
|
||||||
|
form_fields: std::collections::HashMap::from_iter(
|
||||||
|
url.query_pairs()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string())),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(_, _) => None,
|
||||||
|
};
|
||||||
|
(
|
||||||
|
enums::AttemptStatus::foreign_from((data.status, data.next_action)),
|
||||||
|
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
|
resource_id: types::ResponseId::ConnectorTransactionId(data.id), //transaction_id is also the field but this id is used to initiate a refund
|
||||||
|
redirect: redirection_data.is_some(),
|
||||||
|
redirection_data,
|
||||||
|
mandate_reference: None,
|
||||||
|
connector_metadata: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"ERROR" => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
status,
|
||||||
|
response,
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize)]
|
||||||
|
pub struct RapydRefundRequest {
|
||||||
|
pub payment: String,
|
||||||
|
pub amount: Option<i64>,
|
||||||
|
pub currency: Option<enums::Currency>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F> TryFrom<&types::RefundsRouterData<F>> for RapydRefundRequest {
|
||||||
|
type Error = error_stack::Report<errors::ParsingError>;
|
||||||
|
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
payment: item.request.connector_transaction_id.to_string(),
|
||||||
|
amount: Some(item.request.amount),
|
||||||
|
currency: Some(item.request.currency),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum RefundStatus {
|
||||||
|
Completed,
|
||||||
|
Error,
|
||||||
|
Rejected,
|
||||||
|
#[default]
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RefundStatus> for enums::RefundStatus {
|
||||||
|
fn from(item: RefundStatus) -> Self {
|
||||||
|
match item {
|
||||||
|
RefundStatus::Completed => Self::Success,
|
||||||
|
RefundStatus::Error | RefundStatus::Rejected => Self::Failure,
|
||||||
|
RefundStatus::Pending => Self::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RefundResponse {
|
||||||
|
pub status: Status,
|
||||||
|
pub data: Option<RefundResponseData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct RefundResponseData {
|
||||||
|
//Some field related to forign exchange and split payment can be added as and when implemented
|
||||||
|
pub id: String,
|
||||||
|
pub payment: String,
|
||||||
|
pub amount: i64,
|
||||||
|
pub currency: enums::Currency,
|
||||||
|
pub status: RefundStatus,
|
||||||
|
pub created_at: Option<i64>,
|
||||||
|
pub failure_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||||
|
for types::RefundsRouterData<api::Execute>
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ParsingError>;
|
||||||
|
fn try_from(
|
||||||
|
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let (connector_refund_id, refund_status) = match item.response.data {
|
||||||
|
Some(data) => (data.id, enums::RefundStatus::from(data.status)),
|
||||||
|
None => (
|
||||||
|
item.response.status.error_code,
|
||||||
|
enums::RefundStatus::Failure,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
response: Ok(types::RefundsResponseData {
|
||||||
|
connector_refund_id,
|
||||||
|
refund_status,
|
||||||
|
}),
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
|
||||||
|
for types::RefundsRouterData<api::RSync>
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ParsingError>;
|
||||||
|
fn try_from(
|
||||||
|
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let (connector_refund_id, refund_status) = match item.response.data {
|
||||||
|
Some(data) => (data.id, enums::RefundStatus::from(data.status)),
|
||||||
|
None => (
|
||||||
|
item.response.status.error_code,
|
||||||
|
enums::RefundStatus::Failure,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
response: Ok(types::RefundsResponseData {
|
||||||
|
connector_refund_id,
|
||||||
|
refund_status,
|
||||||
|
}),
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
pub struct CaptureRequest {
|
||||||
|
amount: Option<i64>,
|
||||||
|
receipt_email: Option<String>,
|
||||||
|
statement_descriptor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::PaymentsCaptureRouterData> for CaptureRequest {
|
||||||
|
type Error = error_stack::Report<errors::ParsingError>;
|
||||||
|
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
amount: item.request.amount_to_capture,
|
||||||
|
receipt_email: None,
|
||||||
|
statement_descriptor: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>>
|
||||||
|
for types::PaymentsCaptureRouterData
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ParsingError>;
|
||||||
|
fn try_from(
|
||||||
|
item: types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let (status, response) = match item.response.status.status.as_str() {
|
||||||
|
"SUCCESS" => match item.response.data {
|
||||||
|
Some(data) => (
|
||||||
|
enums::AttemptStatus::foreign_from((data.status, data.next_action)),
|
||||||
|
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
|
resource_id: types::ResponseId::ConnectorTransactionId(data.id), //transaction_id is also the field but this id is used to initiate a refund
|
||||||
|
redirection_data: None,
|
||||||
|
redirect: false,
|
||||||
|
mandate_reference: None,
|
||||||
|
connector_metadata: None,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"ERROR" => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
status,
|
||||||
|
response,
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<types::PaymentsCancelResponseRouterData<RapydPaymentsResponse>>
|
||||||
|
for types::PaymentsCancelRouterData
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ParsingError>;
|
||||||
|
fn try_from(
|
||||||
|
item: types::PaymentsCancelResponseRouterData<RapydPaymentsResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let (status, response) = match item.response.status.status.as_str() {
|
||||||
|
"SUCCESS" => match item.response.data {
|
||||||
|
Some(data) => (
|
||||||
|
enums::AttemptStatus::foreign_from((data.status, data.next_action)),
|
||||||
|
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
|
resource_id: types::ResponseId::ConnectorTransactionId(data.id), //transaction_id is also the field but this id is used to initiate a refund
|
||||||
|
redirection_data: None,
|
||||||
|
redirect: false,
|
||||||
|
mandate_reference: None,
|
||||||
|
connector_metadata: None,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"ERROR" => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
enums::AttemptStatus::Failure,
|
||||||
|
Err(types::ErrorResponse {
|
||||||
|
code: item.response.status.error_code,
|
||||||
|
message: item.response.status.status,
|
||||||
|
reason: item.response.status.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
status,
|
||||||
|
response,
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,3 +19,6 @@ pub(crate) const NO_ERROR_CODE: &str = "No error code";
|
|||||||
// General purpose base64 engine
|
// General purpose base64 engine
|
||||||
pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
|
pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
|
||||||
base64::engine::general_purpose::STANDARD;
|
base64::engine::general_purpose::STANDARD;
|
||||||
|
|
||||||
|
pub(crate) const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose =
|
||||||
|
base64::engine::general_purpose::URL_SAFE;
|
||||||
|
|||||||
@ -150,6 +150,7 @@ impl ConnectorData {
|
|||||||
"globalpay" => Ok(Box::new(&connector::Globalpay)),
|
"globalpay" => Ok(Box::new(&connector::Globalpay)),
|
||||||
"klarna" => Ok(Box::new(&connector::Klarna)),
|
"klarna" => Ok(Box::new(&connector::Klarna)),
|
||||||
"payu" => Ok(Box::new(&connector::Payu)),
|
"payu" => Ok(Box::new(&connector::Payu)),
|
||||||
|
"rapyd" => Ok(Box::new(&connector::Rapyd)),
|
||||||
"shift4" => Ok(Box::new(&connector::Shift4)),
|
"shift4" => Ok(Box::new(&connector::Shift4)),
|
||||||
"stripe" => Ok(Box::new(&connector::Stripe)),
|
"stripe" => Ok(Box::new(&connector::Stripe)),
|
||||||
"worldline" => Ok(Box::new(&connector::Worldline)),
|
"worldline" => Ok(Box::new(&connector::Worldline)),
|
||||||
|
|||||||
@ -9,6 +9,7 @@ pub(crate) struct ConnectorAuthentication {
|
|||||||
pub fiserv: Option<SignatureKey>,
|
pub fiserv: Option<SignatureKey>,
|
||||||
pub globalpay: Option<HeaderKey>,
|
pub globalpay: Option<HeaderKey>,
|
||||||
pub payu: Option<BodyKey>,
|
pub payu: Option<BodyKey>,
|
||||||
|
pub rapyd: Option<BodyKey>,
|
||||||
pub shift4: Option<HeaderKey>,
|
pub shift4: Option<HeaderKey>,
|
||||||
pub worldpay: Option<HeaderKey>,
|
pub worldpay: Option<HeaderKey>,
|
||||||
pub worldline: Option<SignatureKey>,
|
pub worldline: Option<SignatureKey>,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ mod connector_auth;
|
|||||||
mod fiserv;
|
mod fiserv;
|
||||||
mod globalpay;
|
mod globalpay;
|
||||||
mod payu;
|
mod payu;
|
||||||
|
mod rapyd;
|
||||||
mod shift4;
|
mod shift4;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod worldline;
|
mod worldline;
|
||||||
|
|||||||
144
crates/router/tests/connectors/rapyd.rs
Normal file
144
crates/router/tests/connectors/rapyd.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
use futures::future::OptionFuture;
|
||||||
|
use masking::Secret;
|
||||||
|
use router::types::{self, api, storage::enums};
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
connector_auth,
|
||||||
|
utils::{self, ConnectorActions},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Rapyd;
|
||||||
|
impl ConnectorActions for Rapyd {}
|
||||||
|
impl utils::Connector for Rapyd {
|
||||||
|
fn get_data(&self) -> types::api::ConnectorData {
|
||||||
|
use router::connector::Rapyd;
|
||||||
|
types::api::ConnectorData {
|
||||||
|
connector: Box::new(&Rapyd),
|
||||||
|
connector_name: types::Connector::Rapyd,
|
||||||
|
get_token: types::api::GetToken::Connector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_auth_token(&self) -> types::ConnectorAuthType {
|
||||||
|
types::ConnectorAuthType::from(
|
||||||
|
connector_auth::ConnectorAuthentication::new()
|
||||||
|
.rapyd
|
||||||
|
.expect("Missing connector authentication configuration"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name(&self) -> String {
|
||||||
|
"rapyd".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_only_authorize_payment() {
|
||||||
|
let response = Rapyd {}
|
||||||
|
.authorize_payment(
|
||||||
|
Some(types::PaymentsAuthorizeData {
|
||||||
|
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
|
||||||
|
card_number: Secret::new("4111111111111111".to_string()),
|
||||||
|
card_exp_month: Secret::new("02".to_string()),
|
||||||
|
card_exp_year: Secret::new("2024".to_string()),
|
||||||
|
card_holder_name: Secret::new("John Doe".to_string()),
|
||||||
|
card_cvc: Secret::new("123".to_string()),
|
||||||
|
}),
|
||||||
|
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
|
||||||
|
..utils::PaymentAuthorizeType::default().0
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_authorize_and_capture_payment() {
|
||||||
|
let response = Rapyd {}
|
||||||
|
.make_payment(
|
||||||
|
Some(types::PaymentsAuthorizeData {
|
||||||
|
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
|
||||||
|
card_number: Secret::new("4111111111111111".to_string()),
|
||||||
|
card_exp_month: Secret::new("02".to_string()),
|
||||||
|
card_exp_year: Secret::new("2024".to_string()),
|
||||||
|
card_holder_name: Secret::new("John Doe".to_string()),
|
||||||
|
card_cvc: Secret::new("123".to_string()),
|
||||||
|
}),
|
||||||
|
..utils::PaymentAuthorizeType::default().0
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_capture_already_authorized_payment() {
|
||||||
|
let connector = Rapyd {};
|
||||||
|
let authorize_response = connector.authorize_payment(None, None).await;
|
||||||
|
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
|
||||||
|
let txn_id = utils::get_connector_transaction_id(authorize_response);
|
||||||
|
let response: OptionFuture<_> = txn_id
|
||||||
|
.map(|transaction_id| async move {
|
||||||
|
connector
|
||||||
|
.capture_payment(transaction_id, None, None)
|
||||||
|
.await
|
||||||
|
.status
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
assert_eq!(response.await, Some(enums::AttemptStatus::Charged));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
#[serial]
|
||||||
|
async fn voiding_already_authorized_payment_fails() {
|
||||||
|
let connector = Rapyd {};
|
||||||
|
let authorize_response = connector.authorize_payment(None, None).await;
|
||||||
|
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
|
||||||
|
let txn_id = utils::get_connector_transaction_id(authorize_response);
|
||||||
|
let response: OptionFuture<_> = txn_id
|
||||||
|
.map(|transaction_id| async move {
|
||||||
|
connector
|
||||||
|
.void_payment(transaction_id, None, None)
|
||||||
|
.await
|
||||||
|
.status
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
assert_eq!(response.await, Some(enums::AttemptStatus::Failure)); //rapyd doesn't allow authorize transaction to be voided
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_refund_succeeded_payment() {
|
||||||
|
let connector = Rapyd {};
|
||||||
|
//make a successful payment
|
||||||
|
let response = connector.make_payment(None, None).await;
|
||||||
|
|
||||||
|
//try refund for previous payment
|
||||||
|
if let Some(transaction_id) = utils::get_connector_transaction_id(response) {
|
||||||
|
let response = connector.refund_payment(transaction_id, None, None).await;
|
||||||
|
assert_eq!(
|
||||||
|
response.response.unwrap().refund_status,
|
||||||
|
enums::RefundStatus::Success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_fail_payment_for_incorrect_card_number() {
|
||||||
|
let response = Rapyd {}
|
||||||
|
.make_payment(
|
||||||
|
Some(types::PaymentsAuthorizeData {
|
||||||
|
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
|
||||||
|
card_number: Secret::new("0000000000000000".to_string()),
|
||||||
|
..utils::CCardType::default().0
|
||||||
|
}),
|
||||||
|
..utils::PaymentAuthorizeType::default().0
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(response.response.is_err(), "The Payment pass");
|
||||||
|
}
|
||||||
@ -26,6 +26,10 @@ key1 = "MerchantPosId"
|
|||||||
[globalpay]
|
[globalpay]
|
||||||
api_key = "Bearer MyApiKey"
|
api_key = "Bearer MyApiKey"
|
||||||
|
|
||||||
|
[rapyd]
|
||||||
|
api_key = "access_key"
|
||||||
|
key1 = "secret_key"
|
||||||
|
|
||||||
[fiserv]
|
[fiserv]
|
||||||
api_key = "MyApiKey"
|
api_key = "MyApiKey"
|
||||||
key1 = "MerchantID"
|
key1 = "MerchantID"
|
||||||
|
|||||||
Reference in New Issue
Block a user