mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
Rapyd webhook integration (#435)
This commit is contained in:
@ -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)?;
|
||||
|
||||
@ -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, "", ×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(
|
||||
&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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)),
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(data.id), //transaction_id is also the field but this id is used to initiate a refund
|
||||
redirection_data: None,
|
||||
redirect: false,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
),
|
||||
None => (
|
||||
enums::AttemptStatus::Failure,
|
||||
Err(types::ErrorResponse {
|
||||
code: item.response.status.error_code,
|
||||
message: item.response.status.status,
|
||||
reason: item.response.status.message,
|
||||
status_code: item.http_code,
|
||||
}),
|
||||
),
|
||||
},
|
||||
"ERROR" => (
|
||||
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.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,
|
||||
}),
|
||||
),
|
||||
_ => (
|
||||
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,
|
||||
}),
|
||||
),
|
||||
};
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)?;
|
||||
|
||||
|
||||
@ -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
|
||||
))?
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user