mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 01:57:45 +08:00 
			
		
		
		
	feat(connector): [Worldpay] add support for webhook (#820)
This commit is contained in:
		| @ -862,10 +862,9 @@ impl api::IncomingWebhook for Nuvei { | |||||||
|         _merchant_id: &str, |         _merchant_id: &str, | ||||||
|         secret: &[u8], |         secret: &[u8], | ||||||
|     ) -> CustomResult<Vec<u8>, errors::ConnectorError> { |     ) -> CustomResult<Vec<u8>, errors::ConnectorError> { | ||||||
|         let body: nuvei::NuveiWebhookDetails = request |         let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params) | ||||||
|             .query_params_json |             .into_report() | ||||||
|             .parse_struct("NuveiWebhookDetails") |             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||||
|             .switch()?; |  | ||||||
|         let secret_str = std::str::from_utf8(secret) |         let secret_str = std::str::from_utf8(secret) | ||||||
|             .into_report() |             .into_report() | ||||||
|             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; |             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||||
| @ -887,10 +886,10 @@ impl api::IncomingWebhook for Nuvei { | |||||||
|         &self, |         &self, | ||||||
|         request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> { |     ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> { | ||||||
|         let body: nuvei::NuveiWebhookTransactionId = request |         let body = | ||||||
|             .query_params_json |             serde_urlencoded::from_str::<nuvei::NuveiWebhookTransactionId>(&request.query_params) | ||||||
|             .parse_struct("NuveiWebhookTransactionId") |                 .into_report() | ||||||
|             .switch()?; |                 .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||||
|         Ok(api_models::webhooks::ObjectReferenceId::PaymentId( |         Ok(api_models::webhooks::ObjectReferenceId::PaymentId( | ||||||
|             types::api::PaymentIdType::ConnectorTransactionId(body.ppp_transaction_id), |             types::api::PaymentIdType::ConnectorTransactionId(body.ppp_transaction_id), | ||||||
|         )) |         )) | ||||||
| @ -900,10 +899,10 @@ impl api::IncomingWebhook for Nuvei { | |||||||
|         &self, |         &self, | ||||||
|         request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { |     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { | ||||||
|         let body: nuvei::NuveiWebhookDataStatus = request |         let body = | ||||||
|             .query_params_json |             serde_urlencoded::from_str::<nuvei::NuveiWebhookDataStatus>(&request.query_params) | ||||||
|             .parse_struct("NuveiWebhookDataStatus") |                 .into_report() | ||||||
|             .switch()?; |                 .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||||
|         match body.status { |         match body.status { | ||||||
|             nuvei::NuveiWebhookStatus::Approved => { |             nuvei::NuveiWebhookStatus::Approved => { | ||||||
|                 Ok(api::IncomingWebhookEvent::PaymentIntentSuccess) |                 Ok(api::IncomingWebhookEvent::PaymentIntentSuccess) | ||||||
| @ -919,10 +918,9 @@ impl api::IncomingWebhook for Nuvei { | |||||||
|         &self, |         &self, | ||||||
|         request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<serde_json::Value, errors::ConnectorError> { |     ) -> CustomResult<serde_json::Value, errors::ConnectorError> { | ||||||
|         let body: nuvei::NuveiWebhookDetails = request |         let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params) | ||||||
|             .query_params_json |             .into_report() | ||||||
|             .parse_struct("NuveiWebhookDetails") |             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||||
|             .switch()?; |  | ||||||
|         let payment_response = nuvei::NuveiPaymentsResponse::from(body); |         let payment_response = nuvei::NuveiPaymentsResponse::from(body); | ||||||
|         Encode::<nuvei::NuveiPaymentsResponse>::encode_to_value(&payment_response).switch() |         Encode::<nuvei::NuveiPaymentsResponse>::encode_to_value(&payment_response).switch() | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -486,15 +486,6 @@ impl common_utils::errors::ErrorSwitch<errors::ConnectorError> for errors::Parsi | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn to_string<T>(data: &T) -> Result<String, Error> |  | ||||||
| where |  | ||||||
|     T: serde::Serialize, |  | ||||||
| { |  | ||||||
|     serde_json::to_string(data) |  | ||||||
|         .into_report() |  | ||||||
|         .change_context(errors::ConnectorError::ResponseHandlingFailed) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub fn base64_decode(data: String) -> Result<Vec<u8>, Error> { | pub fn base64_decode(data: String) -> Result<Vec<u8>, Error> { | ||||||
|     consts::BASE64_ENGINE |     consts::BASE64_ENGINE | ||||||
|         .decode(data) |         .decode(data) | ||||||
|  | |||||||
| @ -4,15 +4,17 @@ mod transformers; | |||||||
|  |  | ||||||
| use std::fmt::Debug; | use std::fmt::Debug; | ||||||
|  |  | ||||||
|  | use common_utils::{crypto, ext_traits::ByteSliceExt}; | ||||||
| use error_stack::{IntoReport, ResultExt}; | use error_stack::{IntoReport, ResultExt}; | ||||||
| use storage_models::enums; | use storage_models::enums; | ||||||
| use transformers as worldpay; | use transformers as worldpay; | ||||||
|  |  | ||||||
| use self::{requests::*, response::*}; | use self::{requests::*, response::*}; | ||||||
| use super::utils::RefundsRequestData; | use super::utils::{self, RefundsRequestData}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     configs::settings, |     configs::settings, | ||||||
|     core::errors::{self, CustomResult}, |     core::errors::{self, CustomResult}, | ||||||
|  |     db::StorageInterface, | ||||||
|     headers, |     headers, | ||||||
|     services::{self, ConnectorIntegration}, |     services::{self, ConnectorIntegration}, | ||||||
|     types::{ |     types::{ | ||||||
| @ -20,7 +22,7 @@ use crate::{ | |||||||
|         api::{self, ConnectorCommon, ConnectorCommonExt}, |         api::{self, ConnectorCommon, ConnectorCommonExt}, | ||||||
|         ErrorResponse, Response, |         ErrorResponse, Response, | ||||||
|     }, |     }, | ||||||
|     utils::{self, BytesExt}, |     utils::{self as ext_traits, BytesExt}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
| @ -384,7 +386,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P | |||||||
|     ) -> CustomResult<Option<String>, errors::ConnectorError> { |     ) -> CustomResult<Option<String>, errors::ConnectorError> { | ||||||
|         let connector_req = WorldpayPaymentsRequest::try_from(req)?; |         let connector_req = WorldpayPaymentsRequest::try_from(req)?; | ||||||
|         let worldpay_req = |         let worldpay_req = | ||||||
|             utils::Encode::<WorldpayPaymentsRequest>::encode_to_string_of_json(&connector_req) |             ext_traits::Encode::<WorldpayPaymentsRequest>::encode_to_string_of_json(&connector_req) | ||||||
|                 .change_context(errors::ConnectorError::RequestEncodingFailed)?; |                 .change_context(errors::ConnectorError::RequestEncodingFailed)?; | ||||||
|         Ok(Some(worldpay_req)) |         Ok(Some(worldpay_req)) | ||||||
|     } |     } | ||||||
| @ -458,8 +460,9 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon | |||||||
|         req: &types::RefundExecuteRouterData, |         req: &types::RefundExecuteRouterData, | ||||||
|     ) -> CustomResult<Option<String>, errors::ConnectorError> { |     ) -> CustomResult<Option<String>, errors::ConnectorError> { | ||||||
|         let connector_req = WorldpayRefundRequest::try_from(req)?; |         let connector_req = WorldpayRefundRequest::try_from(req)?; | ||||||
|         let req = utils::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req) |         let req = | ||||||
|             .change_context(errors::ConnectorError::RequestEncodingFailed)?; |             ext_traits::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req) | ||||||
|  |                 .change_context(errors::ConnectorError::RequestEncodingFailed)?; | ||||||
|         Ok(Some(req)) |         Ok(Some(req)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -593,24 +596,109 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse | |||||||
|  |  | ||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl api::IncomingWebhook for Worldpay { | impl api::IncomingWebhook for Worldpay { | ||||||
|     fn get_webhook_object_reference_id( |     fn get_webhook_source_verification_algorithm( | ||||||
|         &self, |         &self, | ||||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, |         _request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|  |     ) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> { | ||||||
|  |         Ok(Box::new(crypto::Sha256)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_webhook_source_verification_signature( | ||||||
|  |         &self, | ||||||
|  |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|  |     ) -> CustomResult<Vec<u8>, errors::ConnectorError> { | ||||||
|  |         let event_signature = | ||||||
|  |             utils::get_header_key_value("Event-Signature", request.headers)?.split(','); | ||||||
|  |         let sign_header = event_signature | ||||||
|  |             .last() | ||||||
|  |             .ok_or_else(|| errors::ConnectorError::WebhookSignatureNotFound)?; | ||||||
|  |         let signature = sign_header | ||||||
|  |             .split('/') | ||||||
|  |             .last() | ||||||
|  |             .ok_or_else(|| errors::ConnectorError::WebhookSignatureNotFound)?; | ||||||
|  |         hex::decode(signature) | ||||||
|  |             .into_report() | ||||||
|  |             .change_context(errors::ConnectorError::WebhookResponseEncodingFailed) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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, | ||||||
|  |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|  |         _merchant_id: &str, | ||||||
|  |         secret: &[u8], | ||||||
|  |     ) -> CustomResult<Vec<u8>, errors::ConnectorError> { | ||||||
|  |         let secret_str = std::str::from_utf8(secret) | ||||||
|  |             .into_report() | ||||||
|  |             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||||
|  |         let to_sign = format!( | ||||||
|  |             "{}{}", | ||||||
|  |             secret_str, | ||||||
|  |             std::str::from_utf8(request.body) | ||||||
|  |                 .into_report() | ||||||
|  |                 .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? | ||||||
|  |         ); | ||||||
|  |         Ok(to_sign.into_bytes()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_webhook_object_reference_id( | ||||||
|  |         &self, | ||||||
|  |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> { |     ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> { | ||||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() |         let body: WorldpayWebhookTransactionId = request | ||||||
|  |             .body | ||||||
|  |             .parse_struct("WorldpayWebhookTransactionId") | ||||||
|  |             .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; | ||||||
|  |         Ok(api_models::webhooks::ObjectReferenceId::PaymentId( | ||||||
|  |             types::api::PaymentIdType::ConnectorTransactionId( | ||||||
|  |                 body.event_details.transaction_reference, | ||||||
|  |             ), | ||||||
|  |         )) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn get_webhook_event_type( |     fn get_webhook_event_type( | ||||||
|         &self, |         &self, | ||||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { |     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { | ||||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() |         let body: WorldpayWebhookEventType = request | ||||||
|  |             .body | ||||||
|  |             .parse_struct("WorldpayWebhookEventType") | ||||||
|  |             .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; | ||||||
|  |         match body.event_details.event_type { | ||||||
|  |             EventType::SentForSettlement | EventType::Charged => { | ||||||
|  |                 Ok(api::IncomingWebhookEvent::PaymentIntentSuccess) | ||||||
|  |             } | ||||||
|  |             EventType::Error | EventType::Expired => { | ||||||
|  |                 Ok(api::IncomingWebhookEvent::PaymentIntentFailure) | ||||||
|  |             } | ||||||
|  |             _ => Err(errors::ConnectorError::WebhookEventTypeNotFound.into()), | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn get_webhook_resource_object( |     fn get_webhook_resource_object( | ||||||
|         &self, |         &self, | ||||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<serde_json::Value, errors::ConnectorError> { |     ) -> CustomResult<serde_json::Value, errors::ConnectorError> { | ||||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() |         let body: WorldpayWebhookEventType = request | ||||||
|  |             .body | ||||||
|  |             .parse_struct("WorldpayWebhookEventType") | ||||||
|  |             .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; | ||||||
|  |         let psync_body = WorldpayEventResponse::try_from(body)?; | ||||||
|  |         let res_json = serde_json::to_value(psync_body) | ||||||
|  |             .into_report() | ||||||
|  |             .change_context(errors::ConnectorError::WebhookResponseEncodingFailed)?; | ||||||
|  |         Ok(res_json) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -48,6 +48,8 @@ pub enum EventType { | |||||||
|     Refused, |     Refused, | ||||||
|     Refunded, |     Refunded, | ||||||
|     Error, |     Error, | ||||||
|  |     SentForSettlement, | ||||||
|  |     Expired, | ||||||
|     CaptureFailed, |     CaptureFailed, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -305,3 +307,39 @@ pub struct WorldpayErrorResponse { | |||||||
|     pub message: String, |     pub message: String, | ||||||
|     pub validation_errors: Option<serde_json::Value>, |     pub validation_errors: Option<serde_json::Value>, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct WorldpayWebhookTransactionId { | ||||||
|  |     pub event_details: EventDetails, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct EventDetails { | ||||||
|  |     pub transaction_reference: String, | ||||||
|  |     #[serde(rename = "type")] | ||||||
|  |     pub event_type: EventType, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct WorldpayWebhookEventType { | ||||||
|  |     pub event_id: String, | ||||||
|  |     pub event_timestamp: String, | ||||||
|  |     pub event_details: EventDetails, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub enum WorldpayWebhookStatus { | ||||||
|  |     SentForSettlement, | ||||||
|  |     Authorized, | ||||||
|  |     SentForAuthorization, | ||||||
|  |     Cancelled, | ||||||
|  |     Error, | ||||||
|  |     Expired, | ||||||
|  |     Refused, | ||||||
|  |     SentForRefund, | ||||||
|  |     RefundFailed, | ||||||
|  | } | ||||||
|  | |||||||
| @ -122,7 +122,7 @@ impl From<EventType> for enums::AttemptStatus { | |||||||
|             EventType::Authorized => Self::Authorized, |             EventType::Authorized => Self::Authorized, | ||||||
|             EventType::CaptureFailed => Self::CaptureFailed, |             EventType::CaptureFailed => Self::CaptureFailed, | ||||||
|             EventType::Refused => Self::Failure, |             EventType::Refused => Self::Failure, | ||||||
|             EventType::Charged => Self::Charged, |             EventType::Charged | EventType::SentForSettlement => Self::Charged, | ||||||
|             _ => Self::Pending, |             _ => Self::Pending, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -176,3 +176,13 @@ impl<F> TryFrom<&types::RefundsRouterData<F>> for WorldpayRefundRequest { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl TryFrom<WorldpayWebhookEventType> for WorldpayEventResponse { | ||||||
|  |     type Error = error_stack::Report<errors::ConnectorError>; | ||||||
|  |     fn try_from(event: WorldpayWebhookEventType) -> Result<Self, Self::Error> { | ||||||
|  |         Ok(Self { | ||||||
|  |             last_event: event.event_details.event_type, | ||||||
|  |             links: None, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,8 +1,6 @@ | |||||||
| pub mod transformers; | pub mod transformers; | ||||||
| pub mod utils; | pub mod utils; | ||||||
|  |  | ||||||
| use std::collections::HashMap; |  | ||||||
|  |  | ||||||
| use error_stack::{IntoReport, ResultExt}; | use error_stack::{IntoReport, ResultExt}; | ||||||
| use masking::ExposeInterface; | use masking::ExposeInterface; | ||||||
| use router_env::{instrument, tracing}; | use router_env::{instrument, tracing}; | ||||||
| @ -488,20 +486,10 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>( | |||||||
|     .attach_printable("Failed construction of ConnectorData")?; |     .attach_printable("Failed construction of ConnectorData")?; | ||||||
|  |  | ||||||
|     let connector = connector.connector; |     let connector = connector.connector; | ||||||
|     let query_params = Some(req.query_string().to_string()); |  | ||||||
|     let qp: HashMap<String, String> = |  | ||||||
|         url::form_urlencoded::parse(query_params.unwrap_or_default().as_bytes()) |  | ||||||
|             .into_owned() |  | ||||||
|             .collect(); |  | ||||||
|     let json = Encode::<HashMap<String, String>>::encode_to_string_of_json(&qp) |  | ||||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) |  | ||||||
|         .attach_printable("There was an error in parsing the query params")?; |  | ||||||
|  |  | ||||||
|     let mut request_details = api::IncomingWebhookRequestDetails { |     let mut request_details = api::IncomingWebhookRequestDetails { | ||||||
|         method: req.method().clone(), |         method: req.method().clone(), | ||||||
|         headers: req.headers(), |         headers: req.headers(), | ||||||
|         query_params: req.query_string().to_string(), |         query_params: req.query_string().to_string(), | ||||||
|         query_params_json: json.as_bytes(), |  | ||||||
|         body: &body, |         body: &body, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,7 +17,6 @@ pub struct IncomingWebhookRequestDetails<'a> { | |||||||
|     pub headers: &'a actix_web::http::header::HeaderMap, |     pub headers: &'a actix_web::http::header::HeaderMap, | ||||||
|     pub body: &'a [u8], |     pub body: &'a [u8], | ||||||
|     pub query_params: String, |     pub query_params: String, | ||||||
|     pub query_params_json: &'a [u8], |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jagan
					Jagan