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, &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)?;

View File

@ -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, "", &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( 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)
} }
} }

View File

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

View File

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

View File

@ -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
))?
}

View File

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