mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-31 01:57:45 +08:00
feat(connector): Recurly incoming webhook support (#7439)
Co-authored-by: Nishanth Challa <nishanth.challa@Nishanth-Challa-C0WGKCFHLF.local> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: CHALLA NISHANTH BABU <115225644+NISHANTH1221@users.noreply.github.com> Co-authored-by: Aniket Burman <aniket.burman@Aniket-Burman-JDXHW2PH34.local> Co-authored-by: Aniket Burman <aniket.burman@192.168.1.5> Co-authored-by: Aniket Burman <aniket.burman@192.168.1.4>
This commit is contained in:
@ -259,7 +259,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/"
|
||||
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
razorpay.base_url = "https://sandbox.juspay.in/"
|
||||
recurly.base_url = "https://{{merchant_subdomain_name}}.recurly.com"
|
||||
recurly.base_url = "https://v3.recurly.com"
|
||||
redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||
riskified.base_url = "https://sandbox.riskified.com/api"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
|
||||
@ -104,7 +104,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/"
|
||||
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
razorpay.base_url = "https://sandbox.juspay.in/"
|
||||
recurly.base_url = "https://{{merchant_subdomain_name}}.recurly.com"
|
||||
recurly.base_url = "https://v3.recurly.com"
|
||||
redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
signifyd.base_url = "https://api.signifyd.com/"
|
||||
|
||||
@ -108,7 +108,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/"
|
||||
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
razorpay.base_url = "https://api.juspay.in"
|
||||
recurly.base_url = "https://{{merchant_subdomain_name}}.recurly.com"
|
||||
recurly.base_url = "https://v3.recurly.com"
|
||||
redsys.base_url = "https://sis.redsys.es:25443/sis/realizarPago"
|
||||
riskified.base_url = "https://wh.riskified.com/api/"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
|
||||
@ -108,7 +108,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/"
|
||||
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
razorpay.base_url = "https://sandbox.juspay.in/"
|
||||
recurly.base_url = "https://{{merchant_subdomain_name}}.recurly.com"
|
||||
recurly.base_url = "https://v3.recurly.com"
|
||||
redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||
riskified.base_url = "https://sandbox.riskified.com/api"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
|
||||
@ -333,7 +333,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/"
|
||||
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
razorpay.base_url = "https://sandbox.juspay.in/"
|
||||
recurly.base_url = "https://{{merchant_subdomain_name}}.recurly.com"
|
||||
recurly.base_url = "https://v3.recurly.com"
|
||||
redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||
riskified.base_url = "https://sandbox.riskified.com/api"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
|
||||
@ -191,7 +191,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/"
|
||||
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
razorpay.base_url = "https://sandbox.juspay.in/"
|
||||
recurly.base_url = "https://{{merchant_subdomain_name}}.recurly.com"
|
||||
recurly.base_url = "https://v3.recurly.com"
|
||||
redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||
riskified.base_url = "https://sandbox.riskified.com/api"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
|
||||
@ -117,7 +117,7 @@ pub enum RoutableConnectors {
|
||||
Prophetpay,
|
||||
Rapyd,
|
||||
Razorpay,
|
||||
// Recurly,
|
||||
Recurly,
|
||||
// Redsys,
|
||||
Riskified,
|
||||
Shift4,
|
||||
@ -263,7 +263,7 @@ pub enum Connector {
|
||||
Prophetpay,
|
||||
Rapyd,
|
||||
Razorpay,
|
||||
//Recurly,
|
||||
Recurly,
|
||||
// Redsys,
|
||||
Shift4,
|
||||
Square,
|
||||
@ -415,7 +415,7 @@ impl Connector {
|
||||
| Self::Powertranz
|
||||
| Self::Prophetpay
|
||||
| Self::Rapyd
|
||||
// | Self::Recurly
|
||||
| Self::Recurly
|
||||
// | Self::Redsys
|
||||
| Self::Shift4
|
||||
| Self::Square
|
||||
@ -552,7 +552,7 @@ impl From<RoutableConnectors> for Connector {
|
||||
RoutableConnectors::Prophetpay => Self::Prophetpay,
|
||||
RoutableConnectors::Rapyd => Self::Rapyd,
|
||||
RoutableConnectors::Razorpay => Self::Razorpay,
|
||||
// RoutableConnectors::Recurly => Self::Recurly,
|
||||
RoutableConnectors::Recurly => Self::Recurly,
|
||||
RoutableConnectors::Riskified => Self::Riskified,
|
||||
RoutableConnectors::Shift4 => Self::Shift4,
|
||||
RoutableConnectors::Signifyd => Self::Signifyd,
|
||||
|
||||
@ -230,6 +230,7 @@ pub struct ConnectorConfig {
|
||||
pub powertranz: Option<ConnectorTomlConfig>,
|
||||
pub prophetpay: Option<ConnectorTomlConfig>,
|
||||
pub razorpay: Option<ConnectorTomlConfig>,
|
||||
pub recurly: Option<ConnectorTomlConfig>,
|
||||
pub riskified: Option<ConnectorTomlConfig>,
|
||||
pub rapyd: Option<ConnectorTomlConfig>,
|
||||
pub shift4: Option<ConnectorTomlConfig>,
|
||||
@ -400,6 +401,7 @@ impl ConnectorConfig {
|
||||
Connector::Powertranz => Ok(connector_data.powertranz),
|
||||
Connector::Razorpay => Ok(connector_data.razorpay),
|
||||
Connector::Rapyd => Ok(connector_data.rapyd),
|
||||
Connector::Recurly => Ok(connector_data.recurly),
|
||||
Connector::Riskified => Ok(connector_data.riskified),
|
||||
Connector::Shift4 => Ok(connector_data.shift4),
|
||||
Connector::Signifyd => Ok(connector_data.signifyd),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
pub mod transformers;
|
||||
|
||||
use common_utils::{
|
||||
errors::CustomResult,
|
||||
ext_traits::BytesExt,
|
||||
@ -39,7 +38,10 @@ use hyperswitch_interfaces::{
|
||||
use masking::{ExposeInterface, Mask};
|
||||
use transformers as recurly;
|
||||
|
||||
use crate::{constants::headers, types::ResponseRouterData, utils};
|
||||
use crate::{
|
||||
connectors::recurly::transformers::RecurlyWebhookBody, constants::headers,
|
||||
types::ResponseRouterData, utils,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Recurly {
|
||||
@ -52,6 +54,23 @@ impl Recurly {
|
||||
amount_converter: &StringMinorUnitForConnector,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_signature_elements_from_header(
|
||||
headers: &actix_web::http::header::HeaderMap,
|
||||
) -> CustomResult<Vec<Vec<u8>>, errors::ConnectorError> {
|
||||
let security_header = headers
|
||||
.get("recurly-signature")
|
||||
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?;
|
||||
let security_header_str = security_header
|
||||
.to_str()
|
||||
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
|
||||
let header_parts: Vec<Vec<u8>> = security_header_str
|
||||
.split(',')
|
||||
.map(|part| part.trim().as_bytes().to_vec())
|
||||
.collect();
|
||||
|
||||
Ok(header_parts)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Payment for Recurly {}
|
||||
@ -543,6 +562,58 @@ impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Recurly {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl webhooks::IncomingWebhook for Recurly {
|
||||
fn get_webhook_source_verification_algorithm(
|
||||
&self,
|
||||
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Box<dyn common_utils::crypto::VerifySignature + Send>, errors::ConnectorError>
|
||||
{
|
||||
Ok(Box::new(common_utils::crypto::HmacSha256))
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_signature(
|
||||
&self,
|
||||
request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
// The `recurly-signature` header consists of a Unix timestamp (in milliseconds) followed by one or more HMAC-SHA256 signatures, separated by commas.
|
||||
// Multiple signatures exist when a secret key is regenerated, with the old key remaining active for 24 hours.
|
||||
let header_values = Self::get_signature_elements_from_header(request.headers)?;
|
||||
let signature = header_values
|
||||
.get(1)
|
||||
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?;
|
||||
hex::decode(signature).change_context(errors::ConnectorError::WebhookSignatureNotFound)
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_message(
|
||||
&self,
|
||||
request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
_merchant_id: &common_utils::id_type::MerchantId,
|
||||
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let header_values = Self::get_signature_elements_from_header(request.headers)?;
|
||||
let timestamp = header_values
|
||||
.first()
|
||||
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?;
|
||||
Ok(format!(
|
||||
"{}.{}",
|
||||
String::from_utf8_lossy(timestamp),
|
||||
String::from_utf8_lossy(request.body)
|
||||
)
|
||||
.into_bytes())
|
||||
}
|
||||
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
|
||||
fn get_webhook_object_reference_id(
|
||||
&self,
|
||||
request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||
let webhook = RecurlyWebhookBody::get_webhook_object_from_body(request.body)
|
||||
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(webhook.uuid),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "v1", not(all(feature = "revenue_recovery", feature = "v2"))))]
|
||||
fn get_webhook_object_reference_id(
|
||||
&self,
|
||||
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
@ -550,6 +621,25 @@ impl webhooks::IncomingWebhook for Recurly {
|
||||
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api_models::webhooks::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
let webhook = RecurlyWebhookBody::get_webhook_object_from_body(request.body)
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
let event = match webhook.event_type {
|
||||
transformers::RecurlyPaymentEventType::PaymentSucceeded => {
|
||||
api_models::webhooks::IncomingWebhookEvent::RecoveryPaymentSuccess
|
||||
}
|
||||
transformers::RecurlyPaymentEventType::PaymentFailed => {
|
||||
api_models::webhooks::IncomingWebhookEvent::RecoveryPaymentFailure
|
||||
}
|
||||
};
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "v1", not(all(feature = "revenue_recovery", feature = "v2"))))]
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
@ -559,9 +649,11 @@ impl webhooks::IncomingWebhook for Recurly {
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
&self,
|
||||
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
request: &webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
|
||||
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
|
||||
let webhook = RecurlyWebhookBody::get_webhook_object_from_body(request.body)
|
||||
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
|
||||
Ok(Box::new(webhook))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use common_enums::enums;
|
||||
use common_utils::types::StringMinorUnit;
|
||||
use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt, types::StringMinorUnit};
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::{
|
||||
payment_method_data::PaymentMethodData,
|
||||
router_data::{ConnectorAuthType, RouterData},
|
||||
@ -226,3 +227,27 @@ pub struct RecurlyErrorResponse {
|
||||
pub message: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct RecurlyWebhookBody {
|
||||
// transaction id
|
||||
pub uuid: String,
|
||||
pub event_type: RecurlyPaymentEventType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum RecurlyPaymentEventType {
|
||||
#[serde(rename = "succeeded")]
|
||||
PaymentSucceeded,
|
||||
#[serde(rename = "failed")]
|
||||
PaymentFailed,
|
||||
}
|
||||
|
||||
impl RecurlyWebhookBody {
|
||||
pub fn get_webhook_object_from_body(body: &[u8]) -> CustomResult<Self, errors::ConnectorError> {
|
||||
let webhook_body = body
|
||||
.parse_struct::<Self>("RecurlyWebhookBody")
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
Ok(webhook_body)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ use diesel_models::configs;
|
||||
#[cfg(all(any(feature = "v1", feature = "v2"), feature = "olap"))]
|
||||
use diesel_models::{business_profile::CardTestingGuardConfig, organization::OrganizationBridge};
|
||||
use error_stack::{report, FutureExt, ResultExt};
|
||||
use hyperswitch_connectors::connectors::chargebee;
|
||||
use hyperswitch_connectors::connectors::{chargebee, recurly};
|
||||
use hyperswitch_domain_models::merchant_connector_account::{
|
||||
FromRequestEncryptableMerchantConnectorAccount, UpdateEncryptableMerchantConnectorAccount,
|
||||
};
|
||||
@ -1535,6 +1535,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> {
|
||||
razorpay::transformers::RazorpayAuthType::try_from(self.auth_type)?;
|
||||
Ok(())
|
||||
}
|
||||
api_enums::Connector::Recurly => {
|
||||
recurly::transformers::RecurlyAuthType::try_from(self.auth_type)?;
|
||||
Ok(())
|
||||
}
|
||||
api_enums::Connector::Shift4 => {
|
||||
shift4::transformers::Shift4AuthType::try_from(self.auth_type)?;
|
||||
Ok(())
|
||||
|
||||
@ -526,7 +526,9 @@ impl ConnectorData {
|
||||
enums::Connector::Rapyd => {
|
||||
Ok(ConnectorEnum::Old(Box::new(connector::Rapyd::new())))
|
||||
}
|
||||
// enums::Connector::Recurly => Ok(ConnectorEnum::Old(Box::new(connector::Recurly))),
|
||||
enums::Connector::Recurly => {
|
||||
Ok(ConnectorEnum::Old(Box::new(connector::Recurly::new())))
|
||||
}
|
||||
// enums::Connector::Redsys => Ok(ConnectorEnum::Old(Box::new(connector::Redsys))),
|
||||
enums::Connector::Shift4 => {
|
||||
Ok(ConnectorEnum::Old(Box::new(connector::Shift4::new())))
|
||||
|
||||
@ -296,7 +296,7 @@ impl ForeignTryFrom<api_enums::Connector> for common_enums::RoutableConnectors {
|
||||
api_enums::Connector::Prophetpay => Self::Prophetpay,
|
||||
api_enums::Connector::Rapyd => Self::Rapyd,
|
||||
api_enums::Connector::Razorpay => Self::Razorpay,
|
||||
// api_enums::Connector::Recurly => Self::Recurly,
|
||||
api_enums::Connector::Recurly => Self::Recurly,
|
||||
// api_enums::Connector::Redsys => Self::Redsys,
|
||||
api_enums::Connector::Shift4 => Self::Shift4,
|
||||
api_enums::Connector::Signifyd => {
|
||||
|
||||
@ -157,7 +157,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/"
|
||||
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
razorpay.base_url = "https://sandbox.juspay.in/"
|
||||
recurly.base_url = "https://{{merchant_subdomain_name}}.recurly.com"
|
||||
recurly.base_url = "https://v3.recurly.com"
|
||||
redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||
riskified.base_url = "https://sandbox.riskified.com/api"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
|
||||
Reference in New Issue
Block a user