Rapyd webhook integration (#435)

This commit is contained in:
Manoj Ghorela
2023-02-06 14:00:30 +05:30
committed by GitHub
parent 4a820dcd7d
commit a2921ff835
6 changed files with 369 additions and 240 deletions

View File

@ -684,6 +684,8 @@ impl api::IncomingWebhook for Adyen {
&self,
_headers: &actix_web::http::header::HeaderMap,
body: &[u8],
_merchant_id: &str,
_secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let notif = get_webhook_object_from_body(body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

View File

@ -2,7 +2,7 @@ mod transformers;
use std::fmt::Debug;
use base64::Engine;
use common_utils::date_time;
use common_utils::{date_time, ext_traits::StringExt};
use error_stack::{IntoReport, ResultExt};
use rand::distributions::{Alphanumeric, DistString};
use ring::hmac;
@ -10,18 +10,20 @@ use transformers as rapyd;
use crate::{
configs::settings,
connector::utils as conn_utils,
consts,
core::{
errors::{self, CustomResult},
payments,
},
headers, logger, services,
db::StorageInterface,
headers, services,
types::{
self,
api::{self, ConnectorCommon},
ErrorResponse,
},
utils::{self, BytesExt},
utils::{self, crypto, ByteSliceExt, BytesExt},
};
#[derive(Debug, Clone)]
@ -70,6 +72,22 @@ impl ConnectorCommon for Rapyd {
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
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 {}
@ -171,7 +189,6 @@ impl
.response
.parse_struct("Rapyd PaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(rapydpayments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
@ -185,16 +202,7 @@ impl
&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,
reason: response.status.message,
})
self.build_error_response(res)
}
}
@ -283,7 +291,6 @@ impl
.response
.parse_struct("Rapyd PaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(rapydpayments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
@ -297,16 +304,7 @@ impl
&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,
reason: response.status.message,
})
self.build_error_response(res)
}
}
@ -320,7 +318,10 @@ impl
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
) -> 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 {
@ -329,33 +330,74 @@ impl
fn get_url(
&self,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> 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(
&self,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> 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, "", &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::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(
&self,
_res: types::Response,
res: types::Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into())
self.build_error_response(res)
}
fn handle_response(
&self,
_data: &types::PaymentsSyncRouterData,
_res: types::Response,
data: &types::PaymentsSyncRouterData,
res: types::Response,
) -> 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,
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,
reason: response.status.message,
})
self.build_error_response(res)
}
}
@ -559,7 +592,6 @@ impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::Ref
data: &types::RefundsRouterData<api::Execute>,
res: types::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")
@ -577,16 +609,7 @@ impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::Ref
&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,
reason: response.status.message,
})
self.build_error_response(res)
}
}
@ -618,7 +641,6 @@ impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::Refun
data: &types::RefundSyncRouterData,
res: types::Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
logger::debug!(target: "router::connector::rapyd", response=?res);
let response: rapyd::RefundResponse = res
.response
.parse_struct("rapyd RefundResponse")
@ -642,25 +664,145 @@ impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::Refun
#[async_trait::async_trait]
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(
&self,
_body: &[u8],
body: &[u8],
) -> 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(
&self,
_body: &[u8],
body: &[u8],
) -> 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(
&self,
_body: &[u8],
body: &[u8],
) -> 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)
}
}

View File

@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
consts,
core::errors,
pii::{self, Secret},
services,
@ -138,6 +139,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for RapydPaymentsRequest {
}
}
#[derive(Debug, Deserialize)]
pub struct RapydAuthType {
pub access_key: String,
pub secret_key: String,
@ -194,9 +196,10 @@ impl From<transformers::Foreign<(RapydPaymentStatus, String)>>
}
}
RapydPaymentStatus::CanceledByClientOrBank
| RapydPaymentStatus::Error
| RapydPaymentStatus::Expired
| RapydPaymentStatus::ReversedByRapyd => enums::AttemptStatus::Failure,
| RapydPaymentStatus::ReversedByRapyd => enums::AttemptStatus::Voided,
RapydPaymentStatus::Error => enums::AttemptStatus::Failure,
RapydPaymentStatus::New => enums::AttemptStatus::Authorizing,
}
.into()
@ -212,10 +215,10 @@ pub struct RapydPaymentsResponse {
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Status {
pub error_code: String,
pub status: String,
pub status: Option<String>,
pub message: Option<String>,
pub response_code: Option<String>,
pub operation_id: String,
pub operation_id: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -236,83 +239,6 @@ pub struct ResponseData {
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)]
pub struct RapydRefundRequest {
pub payment: String,
@ -435,51 +361,75 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for CaptureRequest {
}
}
impl TryFrom<types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>>
for types::PaymentsCaptureRouterData
impl<F, T>
TryFrom<types::ResponseRouterData<F, RapydPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>,
item: types::ResponseRouterData<F, RapydPaymentsResponse, T, types::PaymentsResponseData>,
) -> 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)),
let (status, response) = match &item.response.data {
Some(data) => {
let attempt_status = enums::AttemptStatus::foreign_from((
data.status.to_owned(),
data.next_action.to_owned(),
));
match attempt_status {
storage_models::enums::AttemptStatus::Failure => (
enums::AttemptStatus::Failure,
Err(types::ErrorResponse {
code: data
.failure_code
.to_owned()
.unwrap_or(item.response.status.error_code),
status_code: item.http_code,
message: item.response.status.status.unwrap_or_default(),
reason: data.failure_message.to_owned(),
}),
),
_ => {
let redirection_data =
match (data.next_action.as_str(), data.redirect_url.to_owned()) {
("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), //transaction_id is also the field but this id is used to initiate a refund
redirection_data: None,
redirect: false,
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,
Err(types::ErrorResponse {
code: item.response.status.error_code,
message: item.response.status.status,
reason: item.response.status.message,
status_code: item.http_code,
}),
),
},
"ERROR" => (
enums::AttemptStatus::Failure,
Err(types::ErrorResponse {
code: item.response.status.error_code,
message: item.response.status.status,
message: item.response.status.status.unwrap_or_default(),
reason: item.response.status.message,
status_code: item.http_code,
}),
),
_ => (
enums::AttemptStatus::Failure,
Err(types::ErrorResponse {
code: item.response.status.error_code,
message: item.response.status.status,
reason: item.response.status.message,
status_code: item.http_code,
}),
),
};
@ -492,59 +442,73 @@ impl TryFrom<types::PaymentsCaptureResponseRouterData<RapydPaymentsResponse>>
}
}
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,
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,
}),
),
};
#[derive(Debug, Deserialize)]
pub struct RapydIncomingWebhook {
pub id: String,
#[serde(rename = "type")]
pub webhook_type: RapydWebhookObjectEventType,
pub data: WebhookData,
pub trigger_operation_id: Option<String>,
pub status: String,
pub created_at: i64,
}
Ok(Self {
status,
response,
..item.data
})
#[derive(Debug, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RapydWebhookObjectEventType {
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),
}
}
}

View File

@ -907,6 +907,8 @@ impl api::IncomingWebhook for Stripe {
&self,
headers: &actix_web::http::header::HeaderMap,
body: &[u8],
_merchant_id: &str,
_secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let mut security_header_kvs = get_signature_elements_from_header(headers)?;

View File

@ -1,8 +1,8 @@
use error_stack::ResultExt;
use error_stack::{report, IntoReport, ResultExt};
use masking::Secret;
use crate::{
core::errors,
core::errors::{self, CustomResult},
pii::PeekInterface,
types::{self, api},
utils::OptionExt,
@ -190,3 +190,20 @@ impl AddressDetailsData for api::AddressDetails {
.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
))?
}

View File

@ -88,6 +88,8 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
&self,
_headers: &actix_web::http::header::HeaderMap,
_body: &[u8],
_merchant_id: &str,
_secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
Ok(Vec::new())
}
@ -106,13 +108,13 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
let signature = self
.get_webhook_source_verification_signature(headers, body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let message = self
.get_webhook_source_verification_message(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)?;
algorithm
.verify_signature(&secret, &signature, &message)