Issue:
Bug: [FEATURE]: [VOLT] Add Support for Webhooks



### Feature Description

Adding Support for Payment Webhooks for Volt Connector

### Have you spent some time checking if this feature request has been raised before?

- [X] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!

Pull request description:
## Type of Change
Support for Webhooks for Volt Connector


- [ ] Bugfix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->


### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
![Screenshot 2024-01-03 at 8 28 35 PM 2](https://github.com/juspay/hyperswitch/assets/85639103/e2a0a038-6b92-4319-a94b-a57d976b205b)

```json
{
    "amount": 6540,
    "currency": "EUR",
    "confirm": true,
      "capture_method": "automatic",
      "capture_on": "2022-09-10T10:11:12Z",
      "amount_to_capture": 6540,
    "customer_id": "StripeCustomer",
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+1",
    "description": "Its my first payment request",
      "authentication_type": "no_three_ds",
    "return_url": "https://google.com",
    "payment_method": "bank_redirect",
    "payment_method_type": "open_banking_uk",
    "payment_method_data": {
        "bank_redirect": {
            "open_banking_uk": {
            }
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "DE",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "DE",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "business_label": "food",
    "business_country": "US"
}
```



## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable


Testing:
![Screenshot 2024-01-03 at 8 28 35 PM 2](https://github.com/juspay/hyperswitch/assets/85639103/e2a0a038-6b92-4319-a94b-a57d976b205b)

```json
{
    "amount": 6540,
    "currency": "EUR",
    "confirm": true,
      "capture_method": "automatic",
      "capture_on": "2022-09-10T10:11:12Z",
      "amount_to_capture": 6540,
    "customer_id": "StripeCustomer",
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+1",
    "description": "Its my first payment request",
      "authentication_type": "no_three_ds",
    "return_url": "https://google.com",
    "payment_method": "bank_redirect",
    "payment_method_type": "open_banking_uk",
    "payment_method_data": {
        "bank_redirect": {
            "open_banking_uk": {
            }
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "DE",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "DE",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "business_label": "food",
    "business_country": "US"
}
```

Patch:
diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs
index bf36a7bff61..3641c0c3ddc 100644
--- a/crates/router/src/connector/volt.rs
+++ b/crates/router/src/connector/volt.rs
@@ -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))
     }
 }
diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs
index 9ee2a3f012e..4c6eaeb52f4 100644
--- a/crates/router/src/connector/volt/transformers.rs
+++ b/crates/router/src/connector/volt/transformers.rs
@@ -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,29 +330,102 @@ 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> {
-        Ok(Self {
-            status: enums::AttemptStatus::from(item.response.status),
-            response: Ok(types::PaymentsResponseData::TransactionResponse {
-                resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()),
-                redirection_data: None,
-                mandate_reference: None,
-                connector_metadata: None,
-                network_txn_id: None,
-                connector_response_reference_id: item
-                    .response
-                    .merchant_internal_reference
-                    .or(Some(item.response.id)),
-                incremental_authorization_allowed: None,
-            }),
-            ..item.data
-        })
+        match item.response {
+            VoltPaymentsResponseData::PsyncResponse(payment_response) => {
+                let status = enums::AttemptStatus::from(payment_response.status.clone());
+                Ok(Self {
+                    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: payment_response
+                                .merchant_internal_reference
+                                .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,
+        }
     }
 }
 
@@ -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,
+    }
+}