mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
Rapyd webhook integration (#435)
This commit is contained in:
@ -684,6 +684,8 @@ impl api::IncomingWebhook for Adyen {
|
|||||||
&self,
|
&self,
|
||||||
_headers: &actix_web::http::header::HeaderMap,
|
_headers: &actix_web::http::header::HeaderMap,
|
||||||
body: &[u8],
|
body: &[u8],
|
||||||
|
_merchant_id: &str,
|
||||||
|
_secret: &[u8],
|
||||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
let notif = get_webhook_object_from_body(body)
|
let notif = get_webhook_object_from_body(body)
|
||||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ mod transformers;
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use common_utils::date_time;
|
use common_utils::{date_time, ext_traits::StringExt};
|
||||||
use error_stack::{IntoReport, ResultExt};
|
use error_stack::{IntoReport, ResultExt};
|
||||||
use rand::distributions::{Alphanumeric, DistString};
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
use ring::hmac;
|
use ring::hmac;
|
||||||
@ -10,18 +10,20 @@ use transformers as rapyd;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::settings,
|
configs::settings,
|
||||||
|
connector::utils as conn_utils,
|
||||||
consts,
|
consts,
|
||||||
core::{
|
core::{
|
||||||
errors::{self, CustomResult},
|
errors::{self, CustomResult},
|
||||||
payments,
|
payments,
|
||||||
},
|
},
|
||||||
headers, logger, services,
|
db::StorageInterface,
|
||||||
|
headers, services,
|
||||||
types::{
|
types::{
|
||||||
self,
|
self,
|
||||||
api::{self, ConnectorCommon},
|
api::{self, ConnectorCommon},
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
},
|
},
|
||||||
utils::{self, BytesExt},
|
utils::{self, crypto, ByteSliceExt, BytesExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -70,6 +72,22 @@ impl ConnectorCommon for Rapyd {
|
|||||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_error_response(
|
||||||
|
&self,
|
||||||
|
res: types::Response,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("Rapyd ErrorResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
Ok(ErrorResponse {
|
||||||
|
status_code: res.status_code,
|
||||||
|
code: response.status.error_code,
|
||||||
|
message: response.status.status.unwrap_or_default(),
|
||||||
|
reason: response.status.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl api::ConnectorAccessToken for Rapyd {}
|
impl api::ConnectorAccessToken for Rapyd {}
|
||||||
@ -171,7 +189,6 @@ impl
|
|||||||
.response
|
.response
|
||||||
.parse_struct("Rapyd PaymentResponse")
|
.parse_struct("Rapyd PaymentResponse")
|
||||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
logger::debug!(rapydpayments_create_response=?response);
|
|
||||||
types::ResponseRouterData {
|
types::ResponseRouterData {
|
||||||
response,
|
response,
|
||||||
data: data.clone(),
|
data: data.clone(),
|
||||||
@ -185,16 +202,7 @@ impl
|
|||||||
&self,
|
&self,
|
||||||
res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
let response: rapyd::RapydPaymentsResponse = res
|
self.build_error_response(res)
|
||||||
.response
|
|
||||||
.parse_struct("Rapyd ErrorResponse")
|
|
||||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
|
||||||
Ok(ErrorResponse {
|
|
||||||
status_code: res.status_code,
|
|
||||||
code: response.status.error_code,
|
|
||||||
message: response.status.status,
|
|
||||||
reason: response.status.message,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +291,6 @@ impl
|
|||||||
.response
|
.response
|
||||||
.parse_struct("Rapyd PaymentResponse")
|
.parse_struct("Rapyd PaymentResponse")
|
||||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
logger::debug!(rapydpayments_create_response=?response);
|
|
||||||
types::ResponseRouterData {
|
types::ResponseRouterData {
|
||||||
response,
|
response,
|
||||||
data: data.clone(),
|
data: data.clone(),
|
||||||
@ -297,16 +304,7 @@ impl
|
|||||||
&self,
|
&self,
|
||||||
res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
let response: rapyd::RapydPaymentsResponse = res
|
self.build_error_response(res)
|
||||||
.response
|
|
||||||
.parse_struct("Rapyd ErrorResponse")
|
|
||||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
|
||||||
Ok(ErrorResponse {
|
|
||||||
status_code: res.status_code,
|
|
||||||
code: response.status.error_code,
|
|
||||||
message: response.status.status,
|
|
||||||
reason: response.status.message,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,7 +318,10 @@ impl
|
|||||||
_req: &types::PaymentsSyncRouterData,
|
_req: &types::PaymentsSyncRouterData,
|
||||||
_connectors: &settings::Connectors,
|
_connectors: &settings::Connectors,
|
||||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
Ok(vec![])
|
Ok(vec![(
|
||||||
|
headers::CONTENT_TYPE.to_string(),
|
||||||
|
types::PaymentsSyncType::get_content_type(self).to_string(),
|
||||||
|
)])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_content_type(&self) -> &'static str {
|
fn get_content_type(&self) -> &'static str {
|
||||||
@ -329,33 +330,74 @@ impl
|
|||||||
|
|
||||||
fn get_url(
|
fn get_url(
|
||||||
&self,
|
&self,
|
||||||
_req: &types::PaymentsSyncRouterData,
|
req: &types::PaymentsSyncRouterData,
|
||||||
_connectors: &settings::Connectors,
|
connectors: &settings::Connectors,
|
||||||
) -> CustomResult<String, errors::ConnectorError> {
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into())
|
let id = req.request.connector_transaction_id.clone();
|
||||||
|
Ok(format!(
|
||||||
|
"{}/v1/payments/{}",
|
||||||
|
self.base_url(connectors),
|
||||||
|
id.get_connector_transaction_id()
|
||||||
|
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_request(
|
fn build_request(
|
||||||
&self,
|
&self,
|
||||||
_req: &types::PaymentsSyncRouterData,
|
req: &types::PaymentsSyncRouterData,
|
||||||
_connectors: &settings::Connectors,
|
connectors: &settings::Connectors,
|
||||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
Ok(None)
|
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 response_id = req.request.connector_transaction_id.clone();
|
||||||
|
let url_path = format!(
|
||||||
|
"/v1/payments/{}",
|
||||||
|
response_id
|
||||||
|
.get_connector_transaction_id()
|
||||||
|
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
|
||||||
|
);
|
||||||
|
let signature = self.generate_signature(&auth, "get", &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::Get)
|
||||||
|
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
|
||||||
|
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
|
||||||
|
.headers(headers)
|
||||||
|
.build();
|
||||||
|
Ok(Some(request))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_error_response(
|
fn get_error_response(
|
||||||
&self,
|
&self,
|
||||||
_res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into())
|
self.build_error_response(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_response(
|
fn handle_response(
|
||||||
&self,
|
&self,
|
||||||
_data: &types::PaymentsSyncRouterData,
|
data: &types::PaymentsSyncRouterData,
|
||||||
_res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||||
Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into())
|
let response: rapyd::RapydPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("Rapyd PaymentResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,16 +503,7 @@ impl
|
|||||||
&self,
|
&self,
|
||||||
res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
let response: rapyd::RapydPaymentsResponse = res
|
self.build_error_response(res)
|
||||||
.response
|
|
||||||
.parse_struct("Rapyd ErrorResponse")
|
|
||||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
|
||||||
Ok(ErrorResponse {
|
|
||||||
status_code: res.status_code,
|
|
||||||
code: response.status.error_code,
|
|
||||||
message: response.status.status,
|
|
||||||
reason: response.status.message,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,7 +592,6 @@ impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::Ref
|
|||||||
data: &types::RefundsRouterData<api::Execute>,
|
data: &types::RefundsRouterData<api::Execute>,
|
||||||
res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
|
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
|
||||||
logger::debug!(target: "router::connector::rapyd", response=?res);
|
|
||||||
let response: rapyd::RefundResponse = res
|
let response: rapyd::RefundResponse = res
|
||||||
.response
|
.response
|
||||||
.parse_struct("rapyd RefundResponse")
|
.parse_struct("rapyd RefundResponse")
|
||||||
@ -577,16 +609,7 @@ impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::Ref
|
|||||||
&self,
|
&self,
|
||||||
res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
let response: rapyd::RapydPaymentsResponse = res
|
self.build_error_response(res)
|
||||||
.response
|
|
||||||
.parse_struct("Rapyd ErrorResponse")
|
|
||||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
|
||||||
Ok(ErrorResponse {
|
|
||||||
status_code: res.status_code,
|
|
||||||
code: response.status.error_code,
|
|
||||||
message: response.status.status,
|
|
||||||
reason: response.status.message,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,7 +641,6 @@ impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::Refun
|
|||||||
data: &types::RefundSyncRouterData,
|
data: &types::RefundSyncRouterData,
|
||||||
res: types::Response,
|
res: types::Response,
|
||||||
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||||
logger::debug!(target: "router::connector::rapyd", response=?res);
|
|
||||||
let response: rapyd::RefundResponse = res
|
let response: rapyd::RefundResponse = res
|
||||||
.response
|
.response
|
||||||
.parse_struct("rapyd RefundResponse")
|
.parse_struct("rapyd RefundResponse")
|
||||||
@ -642,25 +664,145 @@ impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::Refun
|
|||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl api::IncomingWebhook for Rapyd {
|
impl api::IncomingWebhook for Rapyd {
|
||||||
|
fn get_webhook_source_verification_algorithm(
|
||||||
|
&self,
|
||||||
|
_headers: &actix_web::http::header::HeaderMap,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
|
||||||
|
Ok(Box::new(crypto::HmacSha256))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_source_verification_signature(
|
||||||
|
&self,
|
||||||
|
headers: &actix_web::http::header::HeaderMap,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
|
let base64_signature = conn_utils::get_header_key_value("signature", headers)?;
|
||||||
|
let signature = consts::BASE64_ENGINE_URL_SAFE
|
||||||
|
.decode(base64_signature.as_bytes())
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_webhook_source_verification_merchant_secret(
|
||||||
|
&self,
|
||||||
|
db: &dyn StorageInterface,
|
||||||
|
merchant_id: &str,
|
||||||
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
|
let key = format!("wh_mer_sec_verification_{}_{}", self.id(), merchant_id);
|
||||||
|
let secret = db
|
||||||
|
.get_key(&key)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
|
||||||
|
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_source_verification_message(
|
||||||
|
&self,
|
||||||
|
headers: &actix_web::http::header::HeaderMap,
|
||||||
|
body: &[u8],
|
||||||
|
merchant_id: &str,
|
||||||
|
secret: &[u8],
|
||||||
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
|
let host = conn_utils::get_header_key_value("host", headers)?;
|
||||||
|
let connector = self.id();
|
||||||
|
let url_path = format!("https://{host}/webhooks/{merchant_id}/{connector}");
|
||||||
|
let salt = conn_utils::get_header_key_value("salt", headers)?;
|
||||||
|
let timestamp = conn_utils::get_header_key_value("timestamp", headers)?;
|
||||||
|
let stringify_auth = String::from_utf8(secret.to_vec())
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
|
||||||
|
.attach_printable("Could not convert secret to UTF-8")?;
|
||||||
|
let auth: transformers::RapydAuthType = stringify_auth
|
||||||
|
.parse_struct("RapydAuthType")
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
let access_key = auth.access_key;
|
||||||
|
let secret_key = auth.secret_key;
|
||||||
|
let body_string = String::from_utf8(body.to_vec())
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
|
||||||
|
.attach_printable("Could not convert body to UTF-8")?;
|
||||||
|
let to_sign = format!("{url_path}{salt}{timestamp}{access_key}{secret_key}{body_string}");
|
||||||
|
|
||||||
|
Ok(to_sign.into_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_webhook_source(
|
||||||
|
&self,
|
||||||
|
db: &dyn StorageInterface,
|
||||||
|
headers: &actix_web::http::header::HeaderMap,
|
||||||
|
body: &[u8],
|
||||||
|
merchant_id: &str,
|
||||||
|
) -> CustomResult<bool, errors::ConnectorError> {
|
||||||
|
let signature = self
|
||||||
|
.get_webhook_source_verification_signature(headers, body)
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
let secret = self
|
||||||
|
.get_webhook_source_verification_merchant_secret(db, merchant_id)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
let message = self
|
||||||
|
.get_webhook_source_verification_message(headers, body, merchant_id, &secret)
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
|
||||||
|
let stringify_auth = String::from_utf8(secret.to_vec())
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
|
||||||
|
.attach_printable("Could not convert secret to UTF-8")?;
|
||||||
|
let auth: transformers::RapydAuthType = stringify_auth
|
||||||
|
.parse_struct("RapydAuthType")
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
let secret_key = auth.secret_key;
|
||||||
|
let key = hmac::Key::new(hmac::HMAC_SHA256, secret_key.as_bytes());
|
||||||
|
let tag = hmac::sign(&key, &message);
|
||||||
|
let hmac_sign = hex::encode(tag);
|
||||||
|
Ok(hmac_sign.as_bytes().eq(&signature))
|
||||||
|
}
|
||||||
|
|
||||||
fn get_webhook_object_reference_id(
|
fn get_webhook_object_reference_id(
|
||||||
&self,
|
&self,
|
||||||
_body: &[u8],
|
body: &[u8],
|
||||||
) -> CustomResult<String, errors::ConnectorError> {
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
let webhook: transformers::RapydIncomingWebhook = body
|
||||||
|
.parse_struct("RapydIncomingWebhook")
|
||||||
|
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||||
|
Ok(match webhook.data {
|
||||||
|
transformers::WebhookData::PaymentData(payment_data) => payment_data.id,
|
||||||
|
transformers::WebhookData::RefundData(refund_data) => refund_data.id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_webhook_event_type(
|
fn get_webhook_event_type(
|
||||||
&self,
|
&self,
|
||||||
_body: &[u8],
|
body: &[u8],
|
||||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
let webhook: transformers::RapydIncomingWebhook = body
|
||||||
|
.parse_struct("RapydIncomingWebhook")
|
||||||
|
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||||
|
webhook.webhook_type.try_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_webhook_resource_object(
|
fn get_webhook_resource_object(
|
||||||
&self,
|
&self,
|
||||||
_body: &[u8],
|
body: &[u8],
|
||||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
let webhook: transformers::RapydIncomingWebhook = body
|
||||||
|
.parse_struct("RapydIncomingWebhook")
|
||||||
|
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||||
|
let response = match webhook.data {
|
||||||
|
transformers::WebhookData::PaymentData(payment_data) => {
|
||||||
|
let rapyd_response: transformers::RapydPaymentsResponse = payment_data.into();
|
||||||
|
Ok(rapyd_response)
|
||||||
|
}
|
||||||
|
_ => Err(errors::ConnectorError::WebhookEventTypeNotFound),
|
||||||
|
}?;
|
||||||
|
let res_json =
|
||||||
|
utils::Encode::<transformers::RapydPaymentsResponse>::encode_to_value(&response)
|
||||||
|
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
|
||||||
|
|
||||||
|
Ok(res_json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
consts,
|
||||||
core::errors,
|
core::errors,
|
||||||
pii::{self, Secret},
|
pii::{self, Secret},
|
||||||
services,
|
services,
|
||||||
@ -138,6 +139,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for RapydPaymentsRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RapydAuthType {
|
pub struct RapydAuthType {
|
||||||
pub access_key: String,
|
pub access_key: String,
|
||||||
pub secret_key: String,
|
pub secret_key: String,
|
||||||
@ -194,9 +196,10 @@ impl From<transformers::Foreign<(RapydPaymentStatus, String)>>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
RapydPaymentStatus::CanceledByClientOrBank
|
RapydPaymentStatus::CanceledByClientOrBank
|
||||||
| RapydPaymentStatus::Error
|
|
||||||
| RapydPaymentStatus::Expired
|
| RapydPaymentStatus::Expired
|
||||||
| RapydPaymentStatus::ReversedByRapyd => enums::AttemptStatus::Failure,
|
| RapydPaymentStatus::ReversedByRapyd => enums::AttemptStatus::Voided,
|
||||||
|
RapydPaymentStatus::Error => enums::AttemptStatus::Failure,
|
||||||
|
|
||||||
RapydPaymentStatus::New => enums::AttemptStatus::Authorizing,
|
RapydPaymentStatus::New => enums::AttemptStatus::Authorizing,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@ -212,10 +215,10 @@ pub struct RapydPaymentsResponse {
|
|||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub error_code: String,
|
pub error_code: String,
|
||||||
pub status: String,
|
pub status: Option<String>,
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
pub response_code: Option<String>,
|
pub response_code: Option<String>,
|
||||||
pub operation_id: String,
|
pub operation_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
@ -236,83 +239,6 @@ pub struct ResponseData {
|
|||||||
pub failure_message: Option<String>,
|
pub failure_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<types::PaymentsResponseRouterData<RapydPaymentsResponse>>
|
|
||||||
for types::PaymentsAuthorizeRouterData
|
|
||||||
{
|
|
||||||
type Error = error_stack::Report<errors::ConnectorError>;
|
|
||||||
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::ConnectorError::ResponseHandlingFailed)?;
|
|
||||||
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,
|
|
||||||
status_code: item.http_code,
|
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"ERROR" => (
|
|
||||||
enums::AttemptStatus::Failure,
|
|
||||||
Err(types::ErrorResponse {
|
|
||||||
code: item.response.status.error_code,
|
|
||||||
status_code: item.http_code,
|
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
enums::AttemptStatus::Failure,
|
|
||||||
Err(types::ErrorResponse {
|
|
||||||
code: item.response.status.error_code,
|
|
||||||
status_code: item.http_code,
|
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
status,
|
|
||||||
response,
|
|
||||||
..item.data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Serialize)]
|
#[derive(Default, Debug, Serialize)]
|
||||||
pub struct RapydRefundRequest {
|
pub struct RapydRefundRequest {
|
||||||
pub payment: String,
|
pub payment: String,
|
||||||
@ -435,51 +361,75 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for CaptureRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>>
|
impl<F, T>
|
||||||
for types::PaymentsCaptureRouterData
|
TryFrom<types::ResponseRouterData<F, RapydPaymentsResponse, T, types::PaymentsResponseData>>
|
||||||
|
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||||
{
|
{
|
||||||
type Error = error_stack::Report<errors::ConnectorError>;
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
fn try_from(
|
fn try_from(
|
||||||
item: types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>,
|
item: types::ResponseRouterData<F, RapydPaymentsResponse, T, types::PaymentsResponseData>,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
let (status, response) = match item.response.status.status.as_str() {
|
let (status, response) = match &item.response.data {
|
||||||
"SUCCESS" => match item.response.data {
|
Some(data) => {
|
||||||
Some(data) => (
|
let attempt_status = enums::AttemptStatus::foreign_from((
|
||||||
enums::AttemptStatus::foreign_from((data.status, data.next_action)),
|
data.status.to_owned(),
|
||||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
data.next_action.to_owned(),
|
||||||
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,
|
match attempt_status {
|
||||||
redirect: false,
|
storage_models::enums::AttemptStatus::Failure => (
|
||||||
mandate_reference: None,
|
enums::AttemptStatus::Failure,
|
||||||
connector_metadata: None,
|
Err(types::ErrorResponse {
|
||||||
}),
|
code: data
|
||||||
),
|
.failure_code
|
||||||
None => (
|
.to_owned()
|
||||||
enums::AttemptStatus::Failure,
|
.unwrap_or(item.response.status.error_code),
|
||||||
Err(types::ErrorResponse {
|
status_code: item.http_code,
|
||||||
code: item.response.status.error_code,
|
message: item.response.status.status.unwrap_or_default(),
|
||||||
message: item.response.status.status,
|
reason: data.failure_message.to_owned(),
|
||||||
reason: item.response.status.message,
|
}),
|
||||||
status_code: item.http_code,
|
),
|
||||||
}),
|
_ => {
|
||||||
),
|
let redirection_data =
|
||||||
},
|
match (data.next_action.as_str(), data.redirect_url.to_owned()) {
|
||||||
"ERROR" => (
|
("3d_verification", Some(url)) => {
|
||||||
|
let url = Url::parse(&url).into_report().change_context(
|
||||||
|
errors::ConnectorError::ResponseHandlingFailed,
|
||||||
|
)?;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
(
|
||||||
|
attempt_status,
|
||||||
|
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
|
resource_id: types::ResponseId::ConnectorTransactionId(
|
||||||
|
data.id.to_owned(),
|
||||||
|
), //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,
|
enums::AttemptStatus::Failure,
|
||||||
Err(types::ErrorResponse {
|
Err(types::ErrorResponse {
|
||||||
code: item.response.status.error_code,
|
code: item.response.status.error_code,
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
|
||||||
status_code: item.http_code,
|
status_code: item.http_code,
|
||||||
}),
|
message: item.response.status.status.unwrap_or_default(),
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
enums::AttemptStatus::Failure,
|
|
||||||
Err(types::ErrorResponse {
|
|
||||||
code: item.response.status.error_code,
|
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
reason: item.response.status.message,
|
||||||
status_code: item.http_code,
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -492,59 +442,73 @@ impl TryFrom<types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<types::PaymentsCancelResponseRouterData<RapydPaymentsResponse>>
|
#[derive(Debug, Deserialize)]
|
||||||
for types::PaymentsCancelRouterData
|
pub struct RapydIncomingWebhook {
|
||||||
{
|
pub id: String,
|
||||||
type Error = error_stack::Report<errors::ParsingError>;
|
#[serde(rename = "type")]
|
||||||
fn try_from(
|
pub webhook_type: RapydWebhookObjectEventType,
|
||||||
item: types::PaymentsCancelResponseRouterData<RapydPaymentsResponse>,
|
pub data: WebhookData,
|
||||||
) -> Result<Self, Self::Error> {
|
pub trigger_operation_id: Option<String>,
|
||||||
let (status, response) = match item.response.status.status.as_str() {
|
pub status: String,
|
||||||
"SUCCESS" => match item.response.data {
|
pub created_at: i64,
|
||||||
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,
|
|
||||||
status_code: item.http_code,
|
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"ERROR" => (
|
|
||||||
enums::AttemptStatus::Failure,
|
|
||||||
Err(types::ErrorResponse {
|
|
||||||
code: item.response.status.error_code,
|
|
||||||
status_code: item.http_code,
|
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
enums::AttemptStatus::Failure,
|
|
||||||
Err(types::ErrorResponse {
|
|
||||||
code: item.response.status.error_code,
|
|
||||||
status_code: item.http_code,
|
|
||||||
message: item.response.status.status,
|
|
||||||
reason: item.response.status.message,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
#[derive(Debug, Deserialize)]
|
||||||
status,
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
response,
|
pub enum RapydWebhookObjectEventType {
|
||||||
..item.data
|
PaymentCompleted,
|
||||||
})
|
PaymentCaptured,
|
||||||
|
PaymentFailed,
|
||||||
|
RefundCompleted,
|
||||||
|
PaymentRefundRejected,
|
||||||
|
PaymentRefundFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<RapydWebhookObjectEventType> for api::IncomingWebhookEvent {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(value: RapydWebhookObjectEventType) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
RapydWebhookObjectEventType::PaymentCompleted => Ok(Self::PaymentIntentSuccess),
|
||||||
|
RapydWebhookObjectEventType::PaymentCaptured => Ok(Self::PaymentIntentSuccess),
|
||||||
|
RapydWebhookObjectEventType::PaymentFailed => Ok(Self::PaymentIntentFailure),
|
||||||
|
_ => Err(errors::ConnectorError::WebhookEventTypeNotFound).into_report()?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum WebhookData {
|
||||||
|
PaymentData(ResponseData),
|
||||||
|
RefundData(RefundResponseData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ResponseData> for RapydPaymentsResponse {
|
||||||
|
fn from(value: ResponseData) -> Self {
|
||||||
|
Self {
|
||||||
|
status: Status {
|
||||||
|
error_code: consts::NO_ERROR_CODE.to_owned(),
|
||||||
|
status: None,
|
||||||
|
message: None,
|
||||||
|
response_code: None,
|
||||||
|
operation_id: None,
|
||||||
|
},
|
||||||
|
data: Some(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RefundResponseData> for RefundResponse {
|
||||||
|
fn from(value: RefundResponseData) -> Self {
|
||||||
|
Self {
|
||||||
|
status: Status {
|
||||||
|
error_code: consts::NO_ERROR_CODE.to_owned(),
|
||||||
|
status: None,
|
||||||
|
message: None,
|
||||||
|
response_code: None,
|
||||||
|
operation_id: None,
|
||||||
|
},
|
||||||
|
data: Some(value),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -907,6 +907,8 @@ impl api::IncomingWebhook for Stripe {
|
|||||||
&self,
|
&self,
|
||||||
headers: &actix_web::http::header::HeaderMap,
|
headers: &actix_web::http::header::HeaderMap,
|
||||||
body: &[u8],
|
body: &[u8],
|
||||||
|
_merchant_id: &str,
|
||||||
|
_secret: &[u8],
|
||||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
let mut security_header_kvs = get_signature_elements_from_header(headers)?;
|
let mut security_header_kvs = get_signature_elements_from_header(headers)?;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
use error_stack::ResultExt;
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
use masking::Secret;
|
use masking::Secret;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::errors,
|
core::errors::{self, CustomResult},
|
||||||
pii::PeekInterface,
|
pii::PeekInterface,
|
||||||
types::{self, api},
|
types::{self, api},
|
||||||
utils::OptionExt,
|
utils::OptionExt,
|
||||||
@ -190,3 +190,20 @@ impl AddressDetailsData for api::AddressDetails {
|
|||||||
.ok_or_else(missing_field_err("address.country"))
|
.ok_or_else(missing_field_err("address.country"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_header_key_value<'a>(
|
||||||
|
key: &str,
|
||||||
|
headers: &'a actix_web::http::header::HeaderMap,
|
||||||
|
) -> CustomResult<&'a str, errors::ConnectorError> {
|
||||||
|
headers
|
||||||
|
.get(key)
|
||||||
|
.map(|header_value| {
|
||||||
|
header_value
|
||||||
|
.to_str()
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::WebhookSignatureNotFound)
|
||||||
|
})
|
||||||
|
.ok_or(report!(
|
||||||
|
errors::ConnectorError::WebhookSourceVerificationFailed
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
|||||||
@ -88,6 +88,8 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
|
|||||||
&self,
|
&self,
|
||||||
_headers: &actix_web::http::header::HeaderMap,
|
_headers: &actix_web::http::header::HeaderMap,
|
||||||
_body: &[u8],
|
_body: &[u8],
|
||||||
|
_merchant_id: &str,
|
||||||
|
_secret: &[u8],
|
||||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
@ -106,13 +108,13 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
|
|||||||
let signature = self
|
let signature = self
|
||||||
.get_webhook_source_verification_signature(headers, body)
|
.get_webhook_source_verification_signature(headers, body)
|
||||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
let message = self
|
|
||||||
.get_webhook_source_verification_message(headers, body)
|
|
||||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
|
||||||
let secret = self
|
let secret = self
|
||||||
.get_webhook_source_verification_merchant_secret(db, merchant_id)
|
.get_webhook_source_verification_merchant_secret(db, merchant_id)
|
||||||
.await
|
.await
|
||||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
let message = self
|
||||||
|
.get_webhook_source_verification_message(headers, body, merchant_id, &secret)
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
|
||||||
algorithm
|
algorithm
|
||||||
.verify_signature(&secret, &signature, &message)
|
.verify_signature(&secret, &signature, &message)
|
||||||
|
|||||||
Reference in New Issue
Block a user