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, | ||||
|         secret: &[u8], | ||||
|     ) -> CustomResult<Vec<u8>, errors::ConnectorError> { | ||||
|         let body: nuvei::NuveiWebhookDetails = request | ||||
|             .query_params_json | ||||
|             .parse_struct("NuveiWebhookDetails") | ||||
|             .switch()?; | ||||
|         let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params) | ||||
|             .into_report() | ||||
|             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||
|         let secret_str = std::str::from_utf8(secret) | ||||
|             .into_report() | ||||
|             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||
| @ -887,10 +886,10 @@ impl api::IncomingWebhook for Nuvei { | ||||
|         &self, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> { | ||||
|         let body: nuvei::NuveiWebhookTransactionId = request | ||||
|             .query_params_json | ||||
|             .parse_struct("NuveiWebhookTransactionId") | ||||
|             .switch()?; | ||||
|         let body = | ||||
|             serde_urlencoded::from_str::<nuvei::NuveiWebhookTransactionId>(&request.query_params) | ||||
|                 .into_report() | ||||
|                 .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||
|         Ok(api_models::webhooks::ObjectReferenceId::PaymentId( | ||||
|             types::api::PaymentIdType::ConnectorTransactionId(body.ppp_transaction_id), | ||||
|         )) | ||||
| @ -900,10 +899,10 @@ impl api::IncomingWebhook for Nuvei { | ||||
|         &self, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { | ||||
|         let body: nuvei::NuveiWebhookDataStatus = request | ||||
|             .query_params_json | ||||
|             .parse_struct("NuveiWebhookDataStatus") | ||||
|             .switch()?; | ||||
|         let body = | ||||
|             serde_urlencoded::from_str::<nuvei::NuveiWebhookDataStatus>(&request.query_params) | ||||
|                 .into_report() | ||||
|                 .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||
|         match body.status { | ||||
|             nuvei::NuveiWebhookStatus::Approved => { | ||||
|                 Ok(api::IncomingWebhookEvent::PaymentIntentSuccess) | ||||
| @ -919,10 +918,9 @@ impl api::IncomingWebhook for Nuvei { | ||||
|         &self, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> CustomResult<serde_json::Value, errors::ConnectorError> { | ||||
|         let body: nuvei::NuveiWebhookDetails = request | ||||
|             .query_params_json | ||||
|             .parse_struct("NuveiWebhookDetails") | ||||
|             .switch()?; | ||||
|         let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params) | ||||
|             .into_report() | ||||
|             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||
|         let payment_response = nuvei::NuveiPaymentsResponse::from(body); | ||||
|         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> { | ||||
|     consts::BASE64_ENGINE | ||||
|         .decode(data) | ||||
|  | ||||
| @ -4,15 +4,17 @@ mod transformers; | ||||
|  | ||||
| use std::fmt::Debug; | ||||
|  | ||||
| use common_utils::{crypto, ext_traits::ByteSliceExt}; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use storage_models::enums; | ||||
| use transformers as worldpay; | ||||
|  | ||||
| use self::{requests::*, response::*}; | ||||
| use super::utils::RefundsRequestData; | ||||
| use super::utils::{self, RefundsRequestData}; | ||||
| use crate::{ | ||||
|     configs::settings, | ||||
|     core::errors::{self, CustomResult}, | ||||
|     db::StorageInterface, | ||||
|     headers, | ||||
|     services::{self, ConnectorIntegration}, | ||||
|     types::{ | ||||
| @ -20,7 +22,7 @@ use crate::{ | ||||
|         api::{self, ConnectorCommon, ConnectorCommonExt}, | ||||
|         ErrorResponse, Response, | ||||
|     }, | ||||
|     utils::{self, BytesExt}, | ||||
|     utils::{self as ext_traits, BytesExt}, | ||||
| }; | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| @ -384,7 +386,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P | ||||
|     ) -> CustomResult<Option<String>, errors::ConnectorError> { | ||||
|         let connector_req = WorldpayPaymentsRequest::try_from(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)?; | ||||
|         Ok(Some(worldpay_req)) | ||||
|     } | ||||
| @ -458,7 +460,8 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon | ||||
|         req: &types::RefundExecuteRouterData, | ||||
|     ) -> CustomResult<Option<String>, errors::ConnectorError> { | ||||
|         let connector_req = WorldpayRefundRequest::try_from(req)?; | ||||
|         let req = utils::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req) | ||||
|         let req = | ||||
|             ext_traits::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req) | ||||
|                 .change_context(errors::ConnectorError::RequestEncodingFailed)?; | ||||
|         Ok(Some(req)) | ||||
|     } | ||||
| @ -593,24 +596,109 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl api::IncomingWebhook for Worldpay { | ||||
|     fn get_webhook_object_reference_id( | ||||
|     fn get_webhook_source_verification_algorithm( | ||||
|         &self, | ||||
|         _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> { | ||||
|         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( | ||||
|         &self, | ||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> 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( | ||||
|         &self, | ||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> 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, | ||||
|     Refunded, | ||||
|     Error, | ||||
|     SentForSettlement, | ||||
|     Expired, | ||||
|     CaptureFailed, | ||||
| } | ||||
|  | ||||
| @ -305,3 +307,39 @@ pub struct WorldpayErrorResponse { | ||||
|     pub message: String, | ||||
|     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::CaptureFailed => Self::CaptureFailed, | ||||
|             EventType::Refused => Self::Failure, | ||||
|             EventType::Charged => Self::Charged, | ||||
|             EventType::Charged | EventType::SentForSettlement => Self::Charged, | ||||
|             _ => 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 utils; | ||||
|  | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use masking::ExposeInterface; | ||||
| use router_env::{instrument, tracing}; | ||||
| @ -488,20 +486,10 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>( | ||||
|     .attach_printable("Failed construction of ConnectorData")?; | ||||
|  | ||||
|     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 { | ||||
|         method: req.method().clone(), | ||||
|         headers: req.headers(), | ||||
|         query_params: req.query_string().to_string(), | ||||
|         query_params_json: json.as_bytes(), | ||||
|         body: &body, | ||||
|     }; | ||||
|  | ||||
|  | ||||
| @ -17,7 +17,6 @@ pub struct IncomingWebhookRequestDetails<'a> { | ||||
|     pub headers: &'a actix_web::http::header::HeaderMap, | ||||
|     pub body: &'a [u8], | ||||
|     pub query_params: String, | ||||
|     pub query_params_json: &'a [u8], | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jagan
					Jagan