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]
|
||||
base_url = "https://api.shift4.com/"
|
||||
|
||||
[connectors.rapyd]
|
||||
base_url = "https://sandboxapi.rapyd.net"
|
||||
|
||||
[connectors.fiserv]
|
||||
base_url = "https://cert.api.fiservapps.com/"
|
||||
|
||||
@ -90,6 +93,7 @@ base_url = "http://localhost:9090/"
|
||||
|
||||
[connectors.payu]
|
||||
base_url = "https://secure.snd.payu.com/api/"
|
||||
|
||||
[connectors.globalpay]
|
||||
base_url = "https://apis.sandbox.globalpay.com/ucp/"
|
||||
|
||||
|
||||
@ -133,6 +133,9 @@ base_url = "https://apitest.cybersource.com/"
|
||||
[connectors.shift4]
|
||||
base_url = "https://api.shift4.com/"
|
||||
|
||||
[connectors.rapyd]
|
||||
base_url = "https://sandboxapi.rapyd.net"
|
||||
|
||||
[connectors.fiserv]
|
||||
base_url = "https://cert.api.fiservapps.com/"
|
||||
|
||||
|
||||
@ -85,6 +85,9 @@ base_url = "https://apitest.cybersource.com/"
|
||||
[connectors.shift4]
|
||||
base_url = "https://api.shift4.com/"
|
||||
|
||||
[connectors.rapyd]
|
||||
base_url = "https://sandboxapi.rapyd.net"
|
||||
|
||||
[connectors.fiserv]
|
||||
base_url = "https://cert.api.fiservapps.com/"
|
||||
|
||||
|
||||
@ -507,6 +507,7 @@ pub enum Connector {
|
||||
Globalpay,
|
||||
Klarna,
|
||||
Payu,
|
||||
Rapyd,
|
||||
Shift4,
|
||||
Stripe,
|
||||
Worldline,
|
||||
|
||||
@ -63,4 +63,4 @@ max_read_count = 100
|
||||
|
||||
[connectors.supported]
|
||||
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 klarna: ConnectorParams,
|
||||
pub payu: ConnectorParams,
|
||||
pub rapyd: ConnectorParams,
|
||||
pub shift4: ConnectorParams,
|
||||
pub stripe: ConnectorParams,
|
||||
pub supported: SupportedConnectors,
|
||||
|
||||
@ -9,6 +9,7 @@ pub mod fiserv;
|
||||
pub mod globalpay;
|
||||
pub mod klarna;
|
||||
pub mod payu;
|
||||
pub mod rapyd;
|
||||
pub mod shift4;
|
||||
pub mod stripe;
|
||||
pub mod utils;
|
||||
@ -18,6 +19,6 @@ pub mod worldpay;
|
||||
pub use self::{
|
||||
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
||||
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,
|
||||
};
|
||||
|
||||
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
|
||||
pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
|
||||
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)),
|
||||
"klarna" => Ok(Box::new(&connector::Klarna)),
|
||||
"payu" => Ok(Box::new(&connector::Payu)),
|
||||
"rapyd" => Ok(Box::new(&connector::Rapyd)),
|
||||
"shift4" => Ok(Box::new(&connector::Shift4)),
|
||||
"stripe" => Ok(Box::new(&connector::Stripe)),
|
||||
"worldline" => Ok(Box::new(&connector::Worldline)),
|
||||
|
||||
@ -9,6 +9,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub fiserv: Option<SignatureKey>,
|
||||
pub globalpay: Option<HeaderKey>,
|
||||
pub payu: Option<BodyKey>,
|
||||
pub rapyd: Option<BodyKey>,
|
||||
pub shift4: Option<HeaderKey>,
|
||||
pub worldpay: Option<HeaderKey>,
|
||||
pub worldline: Option<SignatureKey>,
|
||||
|
||||
@ -7,6 +7,7 @@ mod connector_auth;
|
||||
mod fiserv;
|
||||
mod globalpay;
|
||||
mod payu;
|
||||
mod rapyd;
|
||||
mod shift4;
|
||||
mod utils;
|
||||
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]
|
||||
api_key = "Bearer MyApiKey"
|
||||
|
||||
[rapyd]
|
||||
api_key = "access_key"
|
||||
key1 = "secret_key"
|
||||
|
||||
[fiserv]
|
||||
api_key = "MyApiKey"
|
||||
key1 = "MerchantID"
|
||||
|
||||
Reference in New Issue
Block a user