mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +08:00 
			
		
		
		
	feat(Connector): [VOLT] Add support for Payments Webhooks (#3155)
This commit is contained in:
		| @ -2,11 +2,13 @@ pub mod transformers; | ||||
|  | ||||
| use std::fmt::Debug; | ||||
|  | ||||
| use common_utils::request::RequestContent; | ||||
| use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use masking::{ExposeInterface, PeekInterface}; | ||||
| use transformers as volt; | ||||
|  | ||||
| use self::transformers::webhook_headers; | ||||
| use super::utils; | ||||
| use crate::{ | ||||
|     configs::settings, | ||||
|     core::errors::{self, CustomResult}, | ||||
| @ -398,7 +400,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe | ||||
|         data: &types::PaymentsSyncRouterData, | ||||
|         res: Response, | ||||
|     ) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> { | ||||
|         let response: volt::VoltPsyncResponse = res | ||||
|         let response: volt::VoltPaymentsResponseData = res | ||||
|             .response | ||||
|             .parse_struct("volt PaymentsSyncResponse") | ||||
|             .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; | ||||
| @ -586,24 +588,93 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl api::IncomingWebhook for Volt { | ||||
|     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::HmacSha256)) | ||||
|     } | ||||
|  | ||||
|     fn get_webhook_source_verification_signature( | ||||
|         &self, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|         _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, | ||||
|     ) -> CustomResult<Vec<u8>, errors::ConnectorError> { | ||||
|         let signature = | ||||
|             utils::get_header_key_value(webhook_headers::X_VOLT_SIGNED, request.headers) | ||||
|                 .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; | ||||
|  | ||||
|         hex::decode(signature) | ||||
|             .into_report() | ||||
|             .change_context(errors::ConnectorError::WebhookVerificationSecretInvalid) | ||||
|     } | ||||
|  | ||||
|     fn get_webhook_source_verification_message( | ||||
|         &self, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|         _merchant_id: &str, | ||||
|         _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, | ||||
|     ) -> CustomResult<Vec<u8>, errors::ConnectorError> { | ||||
|         let x_volt_timed = | ||||
|             utils::get_header_key_value(webhook_headers::X_VOLT_TIMED, request.headers)?; | ||||
|         let user_agent = utils::get_header_key_value(webhook_headers::USER_AGENT, request.headers)?; | ||||
|         let version = user_agent | ||||
|             .split('/') | ||||
|             .last() | ||||
|             .ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)?; | ||||
|         Ok(format!( | ||||
|             "{}|{}|{}", | ||||
|             String::from_utf8_lossy(request.body), | ||||
|             x_volt_timed, | ||||
|             version | ||||
|         ) | ||||
|         .into_bytes()) | ||||
|     } | ||||
|  | ||||
|     fn get_webhook_object_reference_id( | ||||
|         &self, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> CustomResult<api::webhooks::ObjectReferenceId, errors::ConnectorError> { | ||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() | ||||
|         let webhook_body: volt::VoltWebhookBodyReference = request | ||||
|             .body | ||||
|             .parse_struct("VoltWebhookBodyReference") | ||||
|             .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; | ||||
|         let reference = match webhook_body.merchant_internal_reference { | ||||
|             Some(merchant_internal_reference) => { | ||||
|                 api_models::payments::PaymentIdType::PaymentAttemptId(merchant_internal_reference) | ||||
|             } | ||||
|             None => { | ||||
|                 api_models::payments::PaymentIdType::ConnectorTransactionId(webhook_body.payment) | ||||
|             } | ||||
|         }; | ||||
|         Ok(api_models::webhooks::ObjectReferenceId::PaymentId( | ||||
|             reference, | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn get_webhook_event_type( | ||||
|         &self, | ||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { | ||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() | ||||
|         if request.body.is_empty() { | ||||
|             Ok(api::IncomingWebhookEvent::EndpointVerification) | ||||
|         } else { | ||||
|             let payload: volt::VoltWebhookBodyEventType = request | ||||
|                 .body | ||||
|                 .parse_struct("VoltWebhookBodyEventType") | ||||
|                 .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; | ||||
|             Ok(api::IncomingWebhookEvent::from(payload.status)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn get_webhook_resource_object( | ||||
|         &self, | ||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> { | ||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() | ||||
|         let details: volt::VoltWebhookObjectResource = request | ||||
|             .body | ||||
|             .parse_struct("VoltWebhookObjectResource") | ||||
|             .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; | ||||
|         Ok(Box::new(details)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{ | ||||
|     connector::utils::{self, AddressDetailsData, RouterData}, | ||||
|     consts, | ||||
|     core::errors, | ||||
|     services, | ||||
|     types::{self, api, storage::enums as storage_enums}, | ||||
| @ -41,6 +42,12 @@ impl<T> | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub mod webhook_headers { | ||||
|     pub const X_VOLT_SIGNED: &str = "X-Volt-Signed"; | ||||
|     pub const X_VOLT_TIMED: &str = "X-Volt-Timed"; | ||||
|     pub const USER_AGENT: &str = "User-Agent"; | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct VoltPaymentsRequest { | ||||
| @ -50,7 +57,6 @@ pub struct VoltPaymentsRequest { | ||||
|     transaction_type: TransactionType, | ||||
|     merchant_internal_reference: String, | ||||
|     shopper: ShopperDetails, | ||||
|     notification_url: Option<String>, | ||||
|     payment_success_url: Option<String>, | ||||
|     payment_failure_url: Option<String>, | ||||
|     payment_pending_url: Option<String>, | ||||
| @ -91,7 +97,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | ||||
|                     let payment_failure_url = item.router_data.request.router_return_url.clone(); | ||||
|                     let payment_pending_url = item.router_data.request.router_return_url.clone(); | ||||
|                     let payment_cancel_url = item.router_data.request.router_return_url.clone(); | ||||
|                     let notification_url = item.router_data.request.webhook_url.clone(); | ||||
|                     let address = item.router_data.get_billing_address()?; | ||||
|                     let shopper = ShopperDetails { | ||||
|                         email: item.router_data.request.email.clone(), | ||||
| @ -109,7 +114,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | ||||
|                         payment_failure_url, | ||||
|                         payment_pending_url, | ||||
|                         payment_cancel_url, | ||||
|                         notification_url, | ||||
|                         shopper, | ||||
|                         transaction_type, | ||||
|                     }) | ||||
| @ -291,8 +295,9 @@ impl<F, T> | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[derive(Debug, Serialize, Clone, Deserialize)] | ||||
| #[serde(rename_all = "SCREAMING_SNAKE_CASE")] | ||||
| #[derive(strum::Display)] | ||||
| pub enum VoltPaymentStatus { | ||||
|     NewPayment, | ||||
|     Completed, | ||||
| @ -309,7 +314,15 @@ pub enum VoltPaymentStatus { | ||||
|     Failed, | ||||
|     Settled, | ||||
| } | ||||
| #[derive(Debug, Deserialize)] | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| #[serde(untagged)] | ||||
| pub enum VoltPaymentsResponseData { | ||||
|     WebhookResponse(VoltWebhookObjectResource), | ||||
|     PsyncResponse(VoltPsyncResponse), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Clone, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct VoltPsyncResponse { | ||||
|     status: VoltPaymentStatus, | ||||
| @ -317,30 +330,103 @@ pub struct VoltPsyncResponse { | ||||
|     merchant_internal_reference: Option<String>, | ||||
| } | ||||
|  | ||||
| impl<F, T> TryFrom<types::ResponseRouterData<F, VoltPsyncResponse, T, types::PaymentsResponseData>> | ||||
| impl<F, T> | ||||
|     TryFrom<types::ResponseRouterData<F, VoltPaymentsResponseData, T, types::PaymentsResponseData>> | ||||
|     for types::RouterData<F, T, types::PaymentsResponseData> | ||||
| { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from( | ||||
|         item: types::ResponseRouterData<F, VoltPsyncResponse, T, types::PaymentsResponseData>, | ||||
|         item: types::ResponseRouterData< | ||||
|             F, | ||||
|             VoltPaymentsResponseData, | ||||
|             T, | ||||
|             types::PaymentsResponseData, | ||||
|         >, | ||||
|     ) -> Result<Self, Self::Error> { | ||||
|         match item.response { | ||||
|             VoltPaymentsResponseData::PsyncResponse(payment_response) => { | ||||
|                 let status = enums::AttemptStatus::from(payment_response.status.clone()); | ||||
|                 Ok(Self { | ||||
|             status: enums::AttemptStatus::from(item.response.status), | ||||
|             response: Ok(types::PaymentsResponseData::TransactionResponse { | ||||
|                 resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), | ||||
|                     status, | ||||
|                     response: if is_payment_failure(status) { | ||||
|                         Err(types::ErrorResponse { | ||||
|                             code: payment_response.status.clone().to_string(), | ||||
|                             message: payment_response.status.clone().to_string(), | ||||
|                             reason: Some(payment_response.status.to_string()), | ||||
|                             status_code: item.http_code, | ||||
|                             attempt_status: None, | ||||
|                             connector_transaction_id: Some(payment_response.id), | ||||
|                         }) | ||||
|                     } else { | ||||
|                         Ok(types::PaymentsResponseData::TransactionResponse { | ||||
|                             resource_id: types::ResponseId::ConnectorTransactionId( | ||||
|                                 payment_response.id.clone(), | ||||
|                             ), | ||||
|                             redirection_data: None, | ||||
|                             mandate_reference: None, | ||||
|                             connector_metadata: None, | ||||
|                             network_txn_id: None, | ||||
|                 connector_response_reference_id: item | ||||
|                     .response | ||||
|                             connector_response_reference_id: payment_response | ||||
|                                 .merchant_internal_reference | ||||
|                     .or(Some(item.response.id)), | ||||
|                                 .or(Some(payment_response.id)), | ||||
|                             incremental_authorization_allowed: None, | ||||
|             }), | ||||
|                         }) | ||||
|                     }, | ||||
|                     ..item.data | ||||
|                 }) | ||||
|             } | ||||
|             VoltPaymentsResponseData::WebhookResponse(webhook_response) => { | ||||
|                 let detailed_status = webhook_response.detailed_status.clone(); | ||||
|                 let status = enums::AttemptStatus::from(webhook_response.status); | ||||
|                 Ok(Self { | ||||
|                     status, | ||||
|                     response: if is_payment_failure(status) { | ||||
|                         Err(types::ErrorResponse { | ||||
|                             code: detailed_status | ||||
|                                 .clone() | ||||
|                                 .map(|volt_status| volt_status.to_string()) | ||||
|                                 .unwrap_or_else(|| consts::NO_ERROR_CODE.to_owned()), | ||||
|                             message: detailed_status | ||||
|                                 .clone() | ||||
|                                 .map(|volt_status| volt_status.to_string()) | ||||
|                                 .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_owned()), | ||||
|                             reason: detailed_status | ||||
|                                 .clone() | ||||
|                                 .map(|volt_status| volt_status.to_string()), | ||||
|                             status_code: item.http_code, | ||||
|                             attempt_status: None, | ||||
|                             connector_transaction_id: Some(webhook_response.payment.clone()), | ||||
|                         }) | ||||
|                     } else { | ||||
|                         Ok(types::PaymentsResponseData::TransactionResponse { | ||||
|                             resource_id: types::ResponseId::ConnectorTransactionId( | ||||
|                                 webhook_response.payment.clone(), | ||||
|                             ), | ||||
|                             redirection_data: None, | ||||
|                             mandate_reference: None, | ||||
|                             connector_metadata: None, | ||||
|                             network_txn_id: None, | ||||
|                             connector_response_reference_id: webhook_response | ||||
|                                 .merchant_internal_reference | ||||
|                                 .or(Some(webhook_response.payment)), | ||||
|                             incremental_authorization_allowed: None, | ||||
|                         }) | ||||
|                     }, | ||||
|                     ..item.data | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<VoltWebhookStatus> for enums::AttemptStatus { | ||||
|     fn from(status: VoltWebhookStatus) -> Self { | ||||
|         match status { | ||||
|             VoltWebhookStatus::Completed | VoltWebhookStatus::Received => Self::Charged, | ||||
|             VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => Self::Failure, | ||||
|             VoltWebhookStatus::Pending => Self::Pending, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // REFUND : | ||||
| @ -405,6 +491,68 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>> | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Clone, Serialize)] | ||||
| pub struct VoltWebhookBodyReference { | ||||
|     pub payment: String, | ||||
|     pub merchant_internal_reference: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct VoltWebhookBodyEventType { | ||||
|     pub status: VoltWebhookStatus, | ||||
|     pub detailed_status: Option<VoltDetailedStatus>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Clone, Serialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct VoltWebhookObjectResource { | ||||
|     pub payment: String, | ||||
|     pub merchant_internal_reference: Option<String>, | ||||
|     pub status: VoltWebhookStatus, | ||||
|     pub detailed_status: Option<VoltDetailedStatus>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Clone, Serialize)] | ||||
| #[serde(rename_all = "SCREAMING_SNAKE_CASE")] | ||||
| pub enum VoltWebhookStatus { | ||||
|     Completed, | ||||
|     Failed, | ||||
|     Pending, | ||||
|     Received, | ||||
|     NotReceived, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Clone, Serialize)] | ||||
| #[serde(rename_all = "SCREAMING_SNAKE_CASE")] | ||||
| #[derive(strum::Display)] | ||||
| pub enum VoltDetailedStatus { | ||||
|     RefusedByRisk, | ||||
|     RefusedByBank, | ||||
|     ErrorAtBank, | ||||
|     CancelledByUser, | ||||
|     AbandonedByUser, | ||||
|     Failed, | ||||
|     Completed, | ||||
|     BankRedirect, | ||||
|     DelayedAtBank, | ||||
|     AwaitingCheckoutAuthorisation, | ||||
| } | ||||
|  | ||||
| impl From<VoltWebhookStatus> for api::IncomingWebhookEvent { | ||||
|     fn from(status: VoltWebhookStatus) -> Self { | ||||
|         match status { | ||||
|             VoltWebhookStatus::Completed | VoltWebhookStatus::Received => { | ||||
|                 Self::PaymentIntentSuccess | ||||
|             } | ||||
|             VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => { | ||||
|                 Self::PaymentIntentFailure | ||||
|             } | ||||
|             VoltWebhookStatus::Pending => Self::PaymentIntentProcessing, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] | ||||
| pub struct VoltErrorResponse { | ||||
|     pub exception: VoltErrorException, | ||||
| @ -429,3 +577,32 @@ pub struct VoltErrorList { | ||||
|     pub property: String, | ||||
|     pub message: String, | ||||
| } | ||||
|  | ||||
| fn is_payment_failure(status: enums::AttemptStatus) -> bool { | ||||
|     match status { | ||||
|         common_enums::AttemptStatus::AuthenticationFailed | ||||
|         | common_enums::AttemptStatus::AuthorizationFailed | ||||
|         | common_enums::AttemptStatus::CaptureFailed | ||||
|         | common_enums::AttemptStatus::VoidFailed | ||||
|         | common_enums::AttemptStatus::Failure => true, | ||||
|         common_enums::AttemptStatus::Started | ||||
|         | common_enums::AttemptStatus::RouterDeclined | ||||
|         | common_enums::AttemptStatus::AuthenticationPending | ||||
|         | common_enums::AttemptStatus::AuthenticationSuccessful | ||||
|         | common_enums::AttemptStatus::Authorized | ||||
|         | common_enums::AttemptStatus::Charged | ||||
|         | common_enums::AttemptStatus::Authorizing | ||||
|         | common_enums::AttemptStatus::CodInitiated | ||||
|         | common_enums::AttemptStatus::Voided | ||||
|         | common_enums::AttemptStatus::VoidInitiated | ||||
|         | common_enums::AttemptStatus::CaptureInitiated | ||||
|         | common_enums::AttemptStatus::AutoRefunded | ||||
|         | common_enums::AttemptStatus::PartialCharged | ||||
|         | common_enums::AttemptStatus::PartialChargedAndChargeable | ||||
|         | common_enums::AttemptStatus::Unresolved | ||||
|         | common_enums::AttemptStatus::Pending | ||||
|         | common_enums::AttemptStatus::PaymentMethodAwaited | ||||
|         | common_enums::AttemptStatus::ConfirmationAwaited | ||||
|         | common_enums::AttemptStatus::DeviceDataCollectionPending => false, | ||||
|     } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Swangi Kumari
					Swangi Kumari