feat(connector_integration): integrate Rapyd connector (#357)

This commit is contained in:
Manoj Ghorela
2023-01-16 23:58:21 +05:30
committed by GitHub
parent 9fbe738427
commit 006e9a8892
15 changed files with 1306 additions and 2 deletions

View File

@ -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/"

View File

@ -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/"

View File

@ -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/"

View File

@ -507,6 +507,7 @@ pub enum Connector {
Globalpay, Globalpay,
Klarna, Klarna,
Payu, Payu,
Rapyd,
Shift4, Shift4,
Stripe, Stripe,
Worldline, Worldline,

View File

@ -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"]

View File

@ -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,

View File

@ -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,
}; };

View 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, &timestamp, &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, "", &timestamp, &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, &timestamp, &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, &timestamp, &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)
}
}

View 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
})
}
}

View File

@ -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;

View File

@ -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)),

View File

@ -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>,

View File

@ -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;

View 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");
}

View File

@ -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"