mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +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
	 Aniket Burman
					Aniket Burman