Bug: add support for relay refund incoming webhooks



Some connectors, like Adyen, do not support a refund sync API, so we need to rely on refund webhooks for the status. To address this, support for relay refund webhooks needs to be added.

To support this use case, a new webhook endpoint needs to be introduced for relay webhooks:
`/webhooks/relay/{merchant_id}/{connector_id_or_name}`

Once the webhook is identified as a relay webhook, it should be handled based on the flow type, such as refunds, payments, etc. For now, only support for relay refund webhooks has been added.
diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs
index e6f4065eb7a..f99e3a450b2 100644
--- a/crates/api_models/src/webhooks.rs
+++ b/crates/api_models/src/webhooks.rs
@@ -120,6 +120,10 @@ pub enum WebhookResponseTracker {
         status: common_enums::MandateStatus,
     },
     NoEffect,
+    Relay {
+        relay_id: common_utils::id_type::RelayId,
+        status: common_enums::RelayStatus,
+    },
 }
 
 impl WebhookResponseTracker {
@@ -132,6 +136,7 @@ impl WebhookResponseTracker {
             Self::NoEffect | Self::Mandate { .. } => None,
             #[cfg(feature = "payouts")]
             Self::Payout { .. } => None,
+            Self::Relay { .. } => None,
         }
     }
 
@@ -144,6 +149,7 @@ impl WebhookResponseTracker {
             Self::NoEffect | Self::Mandate { .. } => None,
             #[cfg(feature = "payouts")]
             Self::Payout { .. } => None,
+            Self::Relay { .. } => None,
         }
     }
 }
diff --git a/crates/common_utils/src/id_type/relay.rs b/crates/common_utils/src/id_type/relay.rs
index 3ad64729fb7..c818671e0e5 100644
--- a/crates/common_utils/src/id_type/relay.rs
+++ b/crates/common_utils/src/id_type/relay.rs
@@ -1,3 +1,5 @@
+use std::str::FromStr;
+
 crate::id_type!(
     RelayId,
     "A type for relay_id that can be used for relay ids"
@@ -11,3 +13,12 @@ crate::impl_queryable_id_type!(RelayId);
 crate::impl_to_sql_from_sql_id_type!(RelayId);
 
 crate::impl_debug_id_type!(RelayId);
+
+impl FromStr for RelayId {
+    type Err = error_stack::Report<crate::errors::ValidationError>;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let cow_string = std::borrow::Cow::Owned(s.to_string());
+        Self::try_from(cow_string)
+    }
+}
diff --git a/crates/diesel_models/src/query/relay.rs b/crates/diesel_models/src/query/relay.rs
index 034446fe6b5..217f6fd734d 100644
--- a/crates/diesel_models/src/query/relay.rs
+++ b/crates/diesel_models/src/query/relay.rs
@@ -1,4 +1,4 @@
-use diesel::{associations::HasTable, ExpressionMethods};
+use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods};
 
 use super::generics;
 use crate::{
@@ -46,4 +46,18 @@ impl Relay {
         )
         .await
     }
+
+    pub async fn find_by_profile_id_connector_reference_id(
+        conn: &PgPooledConn,
+        profile_id: &common_utils::id_type::ProfileId,
+        connector_reference_id: &str,
+    ) -> StorageResult<Self> {
+        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
+            conn,
+            dsl::profile_id
+                .eq(profile_id.to_owned())
+                .and(dsl::connector_reference_id.eq(connector_reference_id.to_owned())),
+        )
+        .await
+    }
 }
diff --git a/crates/hyperswitch_domain_models/src/relay.rs b/crates/hyperswitch_domain_models/src/relay.rs
index 959ac8e7f61..8af58265c39 100644
--- a/crates/hyperswitch_domain_models/src/relay.rs
+++ b/crates/hyperswitch_domain_models/src/relay.rs
@@ -81,7 +81,7 @@ impl RelayUpdate {
         match response {
             Err(error) => Self::ErrorUpdate {
                 error_code: error.code,
-                error_message: error.message,
+                error_message: error.reason.unwrap_or(error.message),
                 status: common_enums::RelayStatus::Failure,
             },
             Ok(response) => Self::StatusUpdate {
diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs
index ff9849958b5..c29c5ff02c0 100644
--- a/crates/router/src/core/webhooks/incoming.rs
+++ b/crates/router/src/core/webhooks/incoming.rs
@@ -22,9 +22,9 @@ use crate::{
     core::{
         api_locking,
         errors::{self, ConnectorErrorExt, CustomResult, RouterResponse, StorageErrorExt},
-        metrics, payments,
-        payments::tokenization,
-        refunds, utils as core_utils,
+        metrics,
+        payments::{self, tokenization},
+        refunds, relay, utils as core_utils,
         webhooks::utils::construct_webhook_router_data,
     },
     db::StorageInterface,
@@ -62,6 +62,7 @@ pub async fn incoming_webhooks_wrapper<W: types::OutgoingWebhookType>(
     key_store: domain::MerchantKeyStore,
     connector_name_or_mca_id: &str,
     body: actix_web::web::Bytes,
+    is_relay_webhook: bool,
 ) -> RouterResponse<serde_json::Value> {
     let start_instant = Instant::now();
     let (application_response, webhooks_response_tracker, serialized_req) =
@@ -73,6 +74,7 @@ pub async fn incoming_webhooks_wrapper<W: types::OutgoingWebhookType>(
             key_store,
             connector_name_or_mca_id,
             body.clone(),
+            is_relay_webhook,
         ))
         .await?;
 
@@ -118,6 +120,7 @@ pub async fn incoming_webhooks_wrapper<W: types::OutgoingWebhookType>(
     Ok(application_response)
 }
 
+#[allow(clippy::too_many_arguments)]
 #[instrument(skip_all)]
 async fn incoming_webhooks_core<W: types::OutgoingWebhookType>(
     state: SessionState,
@@ -127,6 +130,7 @@ async fn incoming_webhooks_core<W: types::OutgoingWebhookType>(
     key_store: domain::MerchantKeyStore,
     connector_name_or_mca_id: &str,
     body: actix_web::web::Bytes,
+    is_relay_webhook: bool,
 ) -> errors::RouterResult<(
     services::ApplicationResponse<serde_json::Value>,
     WebhookResponseTracker,
@@ -361,120 +365,162 @@ async fn incoming_webhooks_core<W: types::OutgoingWebhookType>(
                 id: profile_id.get_string_repr().to_owned(),
             })?;
 
-        let result_response = match flow_type {
-            api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow(
+        // If the incoming webhook is a relay webhook, then we need to trigger the relay webhook flow
+        let result_response = if is_relay_webhook {
+            let relay_webhook_response = Box::pin(relay_incoming_webhook_flow(
                 state.clone(),
-                req_state,
                 merchant_account,
                 business_profile,
                 key_store,
                 webhook_details,
-                source_verified,
-                &connector,
-                &request_details,
                 event_type,
-            ))
-            .await
-            .attach_printable("Incoming webhook flow for payments failed"),
-
-            api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow(
-                state.clone(),
-                merchant_account,
-                business_profile,
-                key_store,
-                webhook_details,
-                connector_name.as_str(),
                 source_verified,
-                event_type,
             ))
             .await
-            .attach_printable("Incoming webhook flow for refunds failed"),
+            .attach_printable("Incoming webhook flow for relay failed");
 
-            api::WebhookFlow::Dispute => Box::pin(disputes_incoming_webhook_flow(
-                state.clone(),
-                merchant_account,
-                business_profile,
-                key_store,
-                webhook_details,
-                source_verified,
-                &connector,
-                &request_details,
-                event_type,
-            ))
-            .await
-            .attach_printable("Incoming webhook flow for disputes failed"),
+            // Using early return ensures unsupported webhooks are acknowledged to the connector
+            if let Some(errors::ApiErrorResponse::NotSupported { .. }) = relay_webhook_response
+                .as_ref()
+                .err()
+                .map(|a| a.current_context())
+            {
+                logger::error!(
+                    webhook_payload =? request_details.body,
+                    "Failed while identifying the event type",
+                );
 
-            api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow(
-                state.clone(),
-                req_state,
-                merchant_account,
-                business_profile,
-                key_store,
-                webhook_details,
-                source_verified,
-            ))
-            .await
-            .attach_printable("Incoming bank-transfer webhook flow failed"),
+                let response = connector
+                        .get_webhook_api_response(&request_details, None)
+                        .switch()
+                        .attach_printable(
+                            "Failed while early return in case of not supported event type in relay webhooks",
+                        )?;
+
+                return Ok((
+                    response,
+                    WebhookResponseTracker::NoEffect,
+                    serde_json::Value::Null,
+                ));
+            };
 
-            api::WebhookFlow::ReturnResponse => Ok(WebhookResponseTracker::NoEffect),
+            relay_webhook_response
+        } else {
+            match flow_type {
+                api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow(
+                    state.clone(),
+                    req_state,
+                    merchant_account,
+                    business_profile,
+                    key_store,
+                    webhook_details,
+                    source_verified,
+                    &connector,
+                    &request_details,
+                    event_type,
+                ))
+                .await
+                .attach_printable("Incoming webhook flow for payments failed"),
 
-            api::WebhookFlow::Mandate => Box::pin(mandates_incoming_webhook_flow(
-                state.clone(),
-                merchant_account,
-                business_profile,
-                key_store,
-                webhook_details,
-                source_verified,
-                event_type,
-            ))
-            .await
-            .attach_printable("Incoming webhook flow for mandates failed"),
+                api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow(
+                    state.clone(),
+                    merchant_account,
+                    business_profile,
+                    key_store,
+                    webhook_details,
+                    connector_name.as_str(),
+                    source_verified,
+                    event_type,
+                ))
+                .await
+                .attach_printable("Incoming webhook flow for refunds failed"),
 
-            api::WebhookFlow::ExternalAuthentication => {
-                Box::pin(external_authentication_incoming_webhook_flow(
+                api::WebhookFlow::Dispute => Box::pin(disputes_incoming_webhook_flow(
+                    state.clone(),
+                    merchant_account,
+                    business_profile,
+                    key_store,
+                    webhook_details,
+                    source_verified,
+                    &connector,
+                    &request_details,
+                    event_type,
+                ))
+                .await
+                .attach_printable("Incoming webhook flow for disputes failed"),
+
+                api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow(
+                    state.clone(),
+                    req_state,
+                    merchant_account,
+                    business_profile,
+                    key_store,
+                    webhook_details,
+                    source_verified,
+                ))
+                .await
+                .attach_printable("Incoming bank-transfer webhook flow failed"),
+
+                api::WebhookFlow::ReturnResponse => Ok(WebhookResponseTracker::NoEffect),
+
+                api::WebhookFlow::Mandate => Box::pin(mandates_incoming_webhook_flow(
+                    state.clone(),
+                    merchant_account,
+                    business_profile,
+                    key_store,
+                    webhook_details,
+                    source_verified,
+                    event_type,
+                ))
+                .await
+                .attach_printable("Incoming webhook flow for mandates failed"),
+
+                api::WebhookFlow::ExternalAuthentication => {
+                    Box::pin(external_authentication_incoming_webhook_flow(
+                        state.clone(),
+                        req_state,
+                        merchant_account,
+                        key_store,
+                        source_verified,
+                        event_type,
+                        &request_details,
+                        &connector,
+                        object_ref_id,
+                        business_profile,
+                        merchant_connector_account,
+                    ))
+                    .await
+                    .attach_printable("Incoming webhook flow for external authentication failed")
+                }
+                api::WebhookFlow::FraudCheck => Box::pin(frm_incoming_webhook_flow(
                     state.clone(),
                     req_state,
                     merchant_account,
                     key_store,
                     source_verified,
                     event_type,
-                    &request_details,
-                    &connector,
                     object_ref_id,
                     business_profile,
-                    merchant_connector_account,
                 ))
                 .await
-                .attach_printable("Incoming webhook flow for external authentication failed")
-            }
-            api::WebhookFlow::FraudCheck => Box::pin(frm_incoming_webhook_flow(
-                state.clone(),
-                req_state,
-                merchant_account,
-                key_store,
-                source_verified,
-                event_type,
-                object_ref_id,
-                business_profile,
-            ))
-            .await
-            .attach_printable("Incoming webhook flow for fraud check failed"),
+                .attach_printable("Incoming webhook flow for fraud check failed"),
 
-            #[cfg(feature = "payouts")]
-            api::WebhookFlow::Payout => Box::pin(payouts_incoming_webhook_flow(
-                state.clone(),
-                merchant_account,
-                business_profile,
-                key_store,
-                webhook_details,
-                event_type,
-                source_verified,
-            ))
-            .await
-            .attach_printable("Incoming webhook flow for payouts failed"),
+                #[cfg(feature = "payouts")]
+                api::WebhookFlow::Payout => Box::pin(payouts_incoming_webhook_flow(
+                    state.clone(),
+                    merchant_account,
+                    business_profile,
+                    key_store,
+                    webhook_details,
+                    event_type,
+                    source_verified,
+                ))
+                .await
+                .attach_printable("Incoming webhook flow for payouts failed"),
 
-            _ => Err(errors::ApiErrorResponse::InternalServerError)
-                .attach_printable("Unsupported Flow Type received in incoming webhooks"),
+                _ => Err(errors::ApiErrorResponse::InternalServerError)
+                    .attach_printable("Unsupported Flow Type received in incoming webhooks"),
+            }
         };
 
         match result_response {
@@ -836,6 +882,97 @@ async fn payouts_incoming_webhook_flow(
     }
 }
 
+async fn relay_refunds_incoming_webhook_flow(
+    state: SessionState,
+    merchant_account: domain::MerchantAccount,
+    business_profile: domain::Profile,
+    merchant_key_store: domain::MerchantKeyStore,
+    webhook_details: api::IncomingWebhookDetails,
+    event_type: webhooks::IncomingWebhookEvent,
+    source_verified: bool,
+) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
+    let db = &*state.store;
+    let key_manager_state = &(&state).into();
+
+    let relay_record = match webhook_details.object_reference_id {
+        webhooks::ObjectReferenceId::RefundId(refund_id_type) => match refund_id_type {
+            webhooks::RefundIdType::RefundId(refund_id) => {
+                let relay_id = common_utils::id_type::RelayId::from_str(&refund_id)
+                    .change_context(errors::ValidationError::IncorrectValueProvided {
+                        field_name: "relay_id",
+                    })
+                    .change_context(errors::ApiErrorResponse::InternalServerError)?;
+
+                db.find_relay_by_id(key_manager_state, &merchant_key_store, &relay_id)
+                    .await
+                    .to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)
+                    .attach_printable("Failed to fetch the relay record")?
+            }
+            webhooks::RefundIdType::ConnectorRefundId(connector_refund_id) => db
+                .find_relay_by_profile_id_connector_reference_id(
+                    key_manager_state,
+                    &merchant_key_store,
+                    business_profile.get_id(),
+                    &connector_refund_id,
+                )
+                .await
+                .to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)
+                .attach_printable("Failed to fetch the relay record")?,
+        },
+        _ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
+            .attach_printable("received a non-refund id when processing relay refund webhooks")?,
+    };
+
+    // if source_verified then update relay status else trigger relay force sync
+    let relay_response = if source_verified {
+        let relay_update = hyperswitch_domain_models::relay::RelayUpdate::StatusUpdate {
+            connector_reference_id: None,
+            status: common_enums::RelayStatus::foreign_try_from(event_type)
+                .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
+                .attach_printable("failed relay refund status mapping from event type")?,
+        };
+        db.update_relay(
+            key_manager_state,
+            &merchant_key_store,
+            relay_record,
+            relay_update,
+        )
+        .await
+        .map(api_models::relay::RelayResponse::from)
+        .to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)
+        .attach_printable("Failed to update relay")?
+    } else {
+        let relay_retrieve_request = api_models::relay::RelayRetrieveRequest {
+            force_sync: true,
+            id: relay_record.id,
+        };
+        let relay_force_sync_response = Box::pin(relay::relay_retrieve(
+            state,
+            merchant_account,
+            Some(business_profile.get_id().clone()),
+            merchant_key_store,
+            relay_retrieve_request,
+        ))
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("Failed to force sync relay")?;
+
+        if let hyperswitch_domain_models::api::ApplicationResponse::Json(response) =
+            relay_force_sync_response
+        {
+            response
+        } else {
+            Err(errors::ApiErrorResponse::InternalServerError)
+                .attach_printable("Unexpected response from force sync relay")?
+        }
+    };
+
+    Ok(WebhookResponseTracker::Relay {
+        relay_id: relay_response.id,
+        status: relay_response.status,
+    })
+}
+
 #[allow(clippy::too_many_arguments)]
 #[instrument(skip_all)]
 async fn refunds_incoming_webhook_flow(
@@ -938,6 +1075,44 @@ async fn refunds_incoming_webhook_flow(
     })
 }
 
+async fn relay_incoming_webhook_flow(
+    state: SessionState,
+    merchant_account: domain::MerchantAccount,
+    business_profile: domain::Profile,
+    merchant_key_store: domain::MerchantKeyStore,
+    webhook_details: api::IncomingWebhookDetails,
+    event_type: webhooks::IncomingWebhookEvent,
+    source_verified: bool,
+) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
+    let flow_type: api::WebhookFlow = event_type.into();
+
+    let result_response = match flow_type {
+        webhooks::WebhookFlow::Refund => Box::pin(relay_refunds_incoming_webhook_flow(
+            state,
+            merchant_account,
+            business_profile,
+            merchant_key_store,
+            webhook_details,
+            event_type,
+            source_verified,
+        ))
+        .await
+        .attach_printable("Incoming webhook flow for relay refund failed")?,
+        webhooks::WebhookFlow::Payment
+        | webhooks::WebhookFlow::Payout
+        | webhooks::WebhookFlow::Dispute
+        | webhooks::WebhookFlow::Subscription
+        | webhooks::WebhookFlow::ReturnResponse
+        | webhooks::WebhookFlow::BankTransfer
+        | webhooks::WebhookFlow::Mandate
+        | webhooks::WebhookFlow::ExternalAuthentication
+        | webhooks::WebhookFlow::FraudCheck => Err(errors::ApiErrorResponse::NotSupported {
+            message: "Relay webhook flow types not supported".to_string(),
+        })?,
+    };
+    Ok(result_response)
+}
+
 async fn get_payment_attempt_from_object_reference_id(
     state: &SessionState,
     object_reference_id: webhooks::ObjectReferenceId,
diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs
index 569cd330a07..5935c8d4979 100644
--- a/crates/router/src/core/webhooks/incoming_v2.rs
+++ b/crates/router/src/core/webhooks/incoming_v2.rs
@@ -56,6 +56,7 @@ pub async fn incoming_webhooks_wrapper<W: types::OutgoingWebhookType>(
     key_store: domain::MerchantKeyStore,
     connector_id: &common_utils::id_type::MerchantConnectorAccountId,
     body: actix_web::web::Bytes,
+    is_relay_webhook: bool,
 ) -> RouterResponse<serde_json::Value> {
     let start_instant = Instant::now();
     let (application_response, webhooks_response_tracker, serialized_req) =
@@ -68,6 +69,7 @@ pub async fn incoming_webhooks_wrapper<W: types::OutgoingWebhookType>(
             key_store,
             connector_id,
             body.clone(),
+            is_relay_webhook,
         ))
         .await?;
 
@@ -124,6 +126,7 @@ async fn incoming_webhooks_core<W: types::OutgoingWebhookType>(
     key_store: domain::MerchantKeyStore,
     connector_id: &common_utils::id_type::MerchantConnectorAccountId,
     body: actix_web::web::Bytes,
+    _is_relay_webhook: bool,
 ) -> errors::RouterResult<(
     services::ApplicationResponse<serde_json::Value>,
     WebhookResponseTracker,
diff --git a/crates/router/src/db/relay.rs b/crates/router/src/db/relay.rs
index 46259679c55..2ead84019d8 100644
--- a/crates/router/src/db/relay.rs
+++ b/crates/router/src/db/relay.rs
@@ -35,6 +35,14 @@ pub trait RelayInterface {
         merchant_key_store: &domain::MerchantKeyStore,
         relay_id: &common_utils::id_type::RelayId,
     ) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError>;
+
+    async fn find_relay_by_profile_id_connector_reference_id(
+        &self,
+        key_manager_state: &KeyManagerState,
+        merchant_key_store: &domain::MerchantKeyStore,
+        profile_id: &common_utils::id_type::ProfileId,
+        connector_reference_id: &str,
+    ) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError>;
 }
 
 #[async_trait::async_trait]
@@ -105,6 +113,30 @@ impl RelayInterface for Store {
             .await
             .change_context(errors::StorageError::DecryptionError)
     }
+
+    async fn find_relay_by_profile_id_connector_reference_id(
+        &self,
+        key_manager_state: &KeyManagerState,
+        merchant_key_store: &domain::MerchantKeyStore,
+        profile_id: &common_utils::id_type::ProfileId,
+        connector_reference_id: &str,
+    ) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError> {
+        let conn = connection::pg_connection_read(self).await?;
+        diesel_models::relay::Relay::find_by_profile_id_connector_reference_id(
+            &conn,
+            profile_id,
+            connector_reference_id,
+        )
+        .await
+        .map_err(|error| report!(errors::StorageError::from(error)))?
+        .convert(
+            key_manager_state,
+            merchant_key_store.key.get_inner(),
+            merchant_key_store.merchant_id.clone().into(),
+        )
+        .await
+        .change_context(errors::StorageError::DecryptionError)
+    }
 }
 
 #[async_trait::async_trait]
@@ -136,6 +168,16 @@ impl RelayInterface for MockDb {
     ) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError> {
         Err(errors::StorageError::MockDbError)?
     }
+
+    async fn find_relay_by_profile_id_connector_reference_id(
+        &self,
+        _key_manager_state: &KeyManagerState,
+        _merchant_key_store: &domain::MerchantKeyStore,
+        _profile_id: &common_utils::id_type::ProfileId,
+        _connector_reference_id: &str,
+    ) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError> {
+        Err(errors::StorageError::MockDbError)?
+    }
 }
 
 #[async_trait::async_trait]
@@ -178,4 +220,21 @@ impl RelayInterface for KafkaStore {
             .find_relay_by_id(key_manager_state, merchant_key_store, relay_id)
             .await
     }
+
+    async fn find_relay_by_profile_id_connector_reference_id(
+        &self,
+        key_manager_state: &KeyManagerState,
+        merchant_key_store: &domain::MerchantKeyStore,
+        profile_id: &common_utils::id_type::ProfileId,
+        connector_reference_id: &str,
+    ) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError> {
+        self.diesel_store
+            .find_relay_by_profile_id_connector_reference_id(
+                key_manager_state,
+                merchant_key_store,
+                profile_id,
+                connector_reference_id,
+            )
+            .await
+    }
 }
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index 4fe9318f64b..8c2bca5a82e 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -142,6 +142,7 @@ pub fn mk_app(
             .service(routes::Customers::server(state.clone()))
             .service(routes::Configs::server(state.clone()))
             .service(routes::MerchantConnectorAccount::server(state.clone()))
+            .service(routes::RelayWebhooks::server(state.clone()))
             .service(routes::Webhooks::server(state.clone()))
             .service(routes::Relay::server(state.clone()));
 
diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs
index 462861d331f..22f5983e4c3 100644
--- a/crates/router/src/routes.rs
+++ b/crates/router/src/routes.rs
@@ -69,7 +69,7 @@ pub use self::app::{
     ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding,
     Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Mandates,
     MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll,
-    Profile, ProfileNew, Refunds, Relay, SessionState, User, Webhooks,
+    Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks,
 };
 #[cfg(feature = "olap")]
 pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents};
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index f8b5ef44567..2a9561c06c8 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1515,6 +1515,20 @@ impl Webhooks {
     }
 }
 
+pub struct RelayWebhooks;
+
+#[cfg(feature = "oltp")]
+impl RelayWebhooks {
+    pub fn server(state: AppState) -> Scope {
+        use api_models::webhooks as webhook_type;
+        web::scope("/webhooks/relay")
+            .app_data(web::Data::new(state))
+            .service(web::resource("/{merchant_id}/{connector_id}").route(
+                web::post().to(receive_incoming_relay_webhook::<webhook_type::OutgoingWebhook>),
+            ))
+    }
+}
+
 #[cfg(all(feature = "oltp", feature = "v2"))]
 impl Webhooks {
     pub fn server(config: AppState) -> Scope {
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 9d7ae1874c1..6550778619a 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -171,6 +171,7 @@ impl From<Flow> for ApiIdentifier {
 
             Flow::FrmFulfillment
             | Flow::IncomingWebhookReceive
+            | Flow::IncomingRelayWebhookReceive
             | Flow::WebhookEventInitialDeliveryAttemptList
             | Flow::WebhookEventDeliveryAttemptList
             | Flow::WebhookEventDeliveryRetry => Self::Webhooks,
diff --git a/crates/router/src/routes/webhooks.rs b/crates/router/src/routes/webhooks.rs
index 5427ac34b4e..1b3f28fd256 100644
--- a/crates/router/src/routes/webhooks.rs
+++ b/crates/router/src/routes/webhooks.rs
@@ -36,6 +36,7 @@ pub async fn receive_incoming_webhook<W: types::OutgoingWebhookType>(
                 auth.key_store,
                 &connector_id_or_name,
                 body.clone(),
+                false,
             )
         },
         &auth::MerchantIdAuth(merchant_id),
@@ -44,6 +45,89 @@ pub async fn receive_incoming_webhook<W: types::OutgoingWebhookType>(
     .await
 }
 
+#[cfg(feature = "v1")]
+#[instrument(skip_all, fields(flow = ?Flow::IncomingRelayWebhookReceive))]
+pub async fn receive_incoming_relay_webhook<W: types::OutgoingWebhookType>(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    body: web::Bytes,
+    path: web::Path<(
+        common_utils::id_type::MerchantId,
+        common_utils::id_type::MerchantConnectorAccountId,
+    )>,
+) -> impl Responder {
+    let flow = Flow::IncomingWebhookReceive;
+    let (merchant_id, connector_id) = path.into_inner();
+    let is_relay_webhook = true;
+
+    Box::pin(api::server_wrap(
+        flow.clone(),
+        state,
+        &req,
+        (),
+        |state, auth, _, req_state| {
+            webhooks::incoming_webhooks_wrapper::<W>(
+                &flow,
+                state.to_owned(),
+                req_state,
+                &req,
+                auth.merchant_account,
+                auth.key_store,
+                connector_id.get_string_repr(),
+                body.clone(),
+                is_relay_webhook,
+            )
+        },
+        &auth::MerchantIdAuth(merchant_id),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+#[cfg(feature = "v2")]
+#[instrument(skip_all, fields(flow = ?Flow::IncomingRelayWebhookReceive))]
+pub async fn receive_incoming_relay_webhook<W: types::OutgoingWebhookType>(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    body: web::Bytes,
+    path: web::Path<(
+        common_utils::id_type::MerchantId,
+        common_utils::id_type::ProfileId,
+        common_utils::id_type::MerchantConnectorAccountId,
+    )>,
+) -> impl Responder {
+    let flow = Flow::IncomingWebhookReceive;
+    let (merchant_id, profile_id, connector_id) = path.into_inner();
+    let is_relay_webhook = true;
+
+    Box::pin(api::server_wrap(
+        flow.clone(),
+        state,
+        &req,
+        (),
+        |state, auth, _, req_state| {
+            webhooks::incoming_webhooks_wrapper::<W>(
+                &flow,
+                state.to_owned(),
+                req_state,
+                &req,
+                auth.merchant_account,
+                auth.profile,
+                auth.key_store,
+                &connector_id,
+                body.clone(),
+                is_relay_webhook,
+            )
+        },
+        &auth::MerchantIdAndProfileIdAuth {
+            merchant_id,
+            profile_id,
+        },
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 #[instrument(skip_all, fields(flow = ?Flow::IncomingWebhookReceive))]
 #[cfg(feature = "v2")]
 pub async fn receive_incoming_webhook<W: types::OutgoingWebhookType>(
@@ -75,6 +159,7 @@ pub async fn receive_incoming_webhook<W: types::OutgoingWebhookType>(
                 auth.key_store,
                 &connector_id,
                 body.clone(),
+                false,
             )
         },
         &auth::MerchantIdAndProfileIdAuth {
diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs
index bbcdfc535df..cb7d3f78f78 100644
--- a/crates/router/src/types/transformers.rs
+++ b/crates/router/src/types/transformers.rs
@@ -662,6 +662,22 @@ impl ForeignTryFrom<api_models::webhooks::IncomingWebhookEvent> for storage_enum
     }
 }
 
+impl ForeignTryFrom<api_models::webhooks::IncomingWebhookEvent> for api_enums::RelayStatus {
+    type Error = errors::ValidationError;
+
+    fn foreign_try_from(
+        value: api_models::webhooks::IncomingWebhookEvent,
+    ) -> Result<Self, Self::Error> {
+        match value {
+            api_models::webhooks::IncomingWebhookEvent::RefundSuccess => Ok(Self::Success),
+            api_models::webhooks::IncomingWebhookEvent::RefundFailure => Ok(Self::Failure),
+            _ => Err(errors::ValidationError::IncorrectValueProvided {
+                field_name: "incoming_webhook_event_type",
+            }),
+        }
+    }
+}
+
 #[cfg(feature = "payouts")]
 impl ForeignTryFrom<api_models::webhooks::IncomingWebhookEvent> for storage_enums::PayoutStatus {
     type Error = errors::ValidationError;
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 10183f75d5b..935efa3c78b 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -537,6 +537,8 @@ pub enum Flow {
     Relay,
     /// Relay retrieve flow
     RelayRetrieve,
+    /// Incoming Relay Webhook Receive
+    IncomingRelayWebhookReceive,
 }
 
 /// Trait for providing generic behaviour to flow metric
diff --git a/migrations/2025-01-07-105739_create_index_for_relay/down.sql b/migrations/2025-01-07-105739_create_index_for_relay/down.sql
new file mode 100644
index 00000000000..8a75d445466
--- /dev/null
+++ b/migrations/2025-01-07-105739_create_index_for_relay/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+DROP INDEX relay_profile_id_connector_reference_id_index;
\ No newline at end of file
diff --git a/migrations/2025-01-07-105739_create_index_for_relay/up.sql b/migrations/2025-01-07-105739_create_index_for_relay/up.sql
new file mode 100644
index 00000000000..5ebe5983c74
--- /dev/null
+++ b/migrations/2025-01-07-105739_create_index_for_relay/up.sql
@@ -0,0 +1,2 @@
+-- Your SQL goes here
+CREATE UNIQUE INDEX relay_profile_id_connector_reference_id_index ON relay (profile_id, connector_reference_id);
\ No newline at end of file

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
Some connectors, like Adyen, do not support a refund sync API, so we need to rely on refund webhooks for the status. To address this, support for relay refund webhooks is being added.

To support this use case, a new webhook endpoint needs to be introduced for relay webhooks:
/webhooks/relay/{merchant_id}/{connector_id_or_name}

Once the webhook is identified as a relay webhook, it should be handled based on the flow type, such as refunds, payments, etc. For now, only support for relay refund webhooks has been added.

### 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).
-->
This change is to update the relay status when we have to rely on the connector webhooks for the refund status.


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
-> Create a merchant connector account for stripe
-> Setup the webhook url in the stripe dashboard
-> Relay webhook testing
-> Make payment through stripe connector. As this relay webhooks can only handle relay refunds webhooks, we need to return 200 OK for the payments webhooks that we received.
```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-tenant-id: hyperswitch' \
--header 'api-key:' \
--data-raw '{
    "amount": 100,
    "amount_to_capture": 100,
    "currency": "USD",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+1",
    "description": "Its my first payment request",
    "authentication_type": "no_three_ds",
    "customer_id": "cu_1736430184",
    "return_url": "http://127.0.0.1:4040",
    "customer_acceptance": {
        "acceptance_type": "offline",
        "accepted_at": "1963-05-03T04:07:52.723Z",
        "online": {
            "ip_address": "in sit",
            "user_agent": "amet irure esse"
        }
    },
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "card_number": "4111111111111111",
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "card_cvc": "737"
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "PiX",
            "last_name": "ss"
        },
        "email": "raj@gmail.com"
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "PiX"
        }
    },
    "browser_info": {
        "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/70.0.3538.110 Safari\/537.36",
        "accept_header": "text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/webp,image\/apng,*\/*;q=0.8",
        "language": "nl-NL",
        "color_depth": 24,
        "screen_height": 723,
        "screen_width": 1536,
        "time_zone": 0,
        "java_enabled": true,
        "java_script_enabled": true,
        "ip_address": "125.0.0.1"
    },
    "routing": {
        "type": "single",
        "data": {
            "connector": "cybersource",
            "merchant_connector_id": "mca_8W0CQfwE89J1BJBbpcnt"
        }
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    }
}'
```
```
{
    "payment_id": "pay_9IYMeErPOtFrHTxEundR",
    "merchant_id": "merchant_1736429319",
    "status": "succeeded",
    "amount": 100,
    "net_amount": 100,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 100,
    "connector": "stripe",
    "client_secret": "pay_9IYMeErPOtFrHTxEundR_secret_A9BwRKPAld2g2fPwpN0Z",
    "created": "2025-01-09T13:42:58.438Z",
    "currency": "USD",
    "customer_id": "cu_1736430178",
    "customer": {
        "id": "cu_1736430178",
        "name": "John Doe",
        "email": "guest@example.com",
        "phone": "999999999",
        "phone_country_code": "+1"
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "payment_checks": {
                "cvc_check": "pass",
                "address_line1_check": "pass",
                "address_postal_code_check": "pass"
            },
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": null
        },
        "phone": null,
        "email": null
    },
    "billing": {
        "address": {
            "city": "San",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": "ss"
        },
        "phone": null,
        "email": "raj@gmail.com"
    },
    "order_details": null,
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "http://127.0.0.1:4040/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": null,
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": {
        "customer_id": "cu_1736430178",
        "created_at": 1736430178,
        "expires": 1736433778,
        "secret": "epk_54d28c2dcea8416fa84ce3ec9f960102"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "pi_3QfM0lD5R7gDAGff1L1eaicn",
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pi_3QfM0lD5R7gDAGff1L1eaicn",
    "payment_link": null,
    "profile_id": "pro_GhoiAnNpnyEAqAAbwULW",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-01-09T13:57:58.438Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "125.0.0.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "java_script_enabled": true
    },
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2025-01-09T13:43:00.565Z",
    "split_payments": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}
```
![image](https://github.com/user-attachments/assets/8f422071-09fa-41a1-9ef3-096baed78c64)

-> Create a relay refund
```
curl --location 'http://localhost:8080/relay' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: pro_GhoiAnNpnyEAqAAbwULW' \
--header 'api-key:' \
--data '{
    "connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "connector_resource_id": "pi_3QfM0lD5R7gDAGff1L1eaicn",
    "data": {
        "refund": {
            "amount": 1,
            "currency": "USD"
        }
    },
    "type": "refund"
}'
```
```
{
    "id": "relay_WhRindDie9fcGbLRvE3G",
    "status": "success",
    "connector_resource_id": "pi_3QfM0lD5R7gDAGff1L1eaicn",
    "error": null,
    "connector_reference_id": "re_3QfM0lD5R7gDAGff1533tuY2",
    "connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "profile_id": "pro_GhoiAnNpnyEAqAAbwULW",
    "type": "refund",
    "data": {
        "refund": {
            "amount": 1,
            "currency": "USD",
            "reason": null
        }
    }
}
```
![image](https://github.com/user-attachments/assets/c9bca26d-2bf5-472b-b38c-686821b08b4a)
-> Manually setting the db recording the pending to test relay status update by webhook
```
curl --location 'http://localhost:8080/relay/relay_WhRindDie9fcGbLRvE3G' \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: <x-idempotency-key>' \
--header 'X-Profile-Id: pro_GhoiAnNpnyEAqAAbwULW' \
--header 'api-key:' \
--data ''
```
```
{
    "id": "relay_WhRindDie9fcGbLRvE3G",
    "status": "pending",
    "connector_resource_id": "pi_3QfM0lD5R7gDAGff1L1eaicn",
    "error": null,
    "connector_reference_id": "re_3QfM0lD5R7gDAGff1533tuY2",
    "connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "profile_id": "pro_GhoiAnNpnyEAqAAbwULW",
    "type": "refund",
    "data": {
        "refund": {
            "amount": 1,
            "currency": "USD",
            "reason": null
        }
    }
}
```

-> Re trigger the relay webhook and when retrieved the status is expected to be succeeded
![image](https://github.com/user-attachments/assets/c860f611-7b81-49d8-85cd-55e7f52ad347)
```
curl --location 'http://localhost:8080/relay/relay_WhRindDie9fcGbLRvE3G' \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: <x-idempotency-key>' \
--header 'X-Profile-Id: pro_GhoiAnNpnyEAqAAbwULW' \
--header 'api-key:' \
--data ''
```
```
{
    "id": "relay_WhRindDie9fcGbLRvE3G",
    "status": "success",
    "connector_resource_id": "pi_3QfM0lD5R7gDAGff1L1eaicn",
    "error": null,
    "connector_reference_id": "re_3QfM0lD5R7gDAGff1533tuY2",
    "connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "profile_id": "pro_GhoiAnNpnyEAqAAbwULW",
    "type": "refund",
    "data": {
        "refund": {
            "amount": 1,
            "currency": "USD",
            "reason": null
        }
    }
}
```

-> Test stripe normal payments webhooks to ensure that the webhook behaviour is not affected for all other events
-> Create a payment
```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-tenant-id: hyperswitch' \
--header 'api-key:' \
--data-raw '{
    "amount": 100,
    "amount_to_capture": 100,
    "currency": "USD",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+1",
    "description": "Its my first payment request",
    "authentication_type": "no_three_ds",
    "customer_id": "cu_1736432186",
    "return_url": "http://127.0.0.1:4040",
    "customer_acceptance": {
        "acceptance_type": "offline",
        "accepted_at": "1963-05-03T04:07:52.723Z",
        "online": {
            "ip_address": "in sit",
            "user_agent": "amet irure esse"
        }
    },
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "card_number": "4111111111111111",
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "card_cvc": "737"
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "PiX",
            "last_name": "ss"
        },
        "email": "raj@gmail.com"
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "PiX"
        }
    },
    "browser_info": {
        "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/70.0.3538.110 Safari\/537.36",
        "accept_header": "text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/webp,image\/apng,*\/*;q=0.8",
        "language": "nl-NL",
        "color_depth": 24,
        "screen_height": 723,
        "screen_width": 1536,
        "time_zone": 0,
        "java_enabled": true,
        "java_script_enabled": true,
        "ip_address": "125.0.0.1"
    },
    "routing": {
        "type": "single",
        "data": {
            "connector": "cybersource",
            "merchant_connector_id": "mca_8W0CQfwE89J1BJBbpcnt"
        }
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    }
}'
```
```
{
    "payment_id": "pay_bnl7rTJB6WUbfsQRTRU3",
    "merchant_id": "merchant_1736429319",
    "status": "succeeded",
    "amount": 100,
    "net_amount": 100,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 100,
    "connector": "stripe",
    "client_secret": "pay_bnl7rTJB6WUbfsQRTRU3_secret_YRCZCEKKcDxD6VnbtBGl",
    "created": "2025-01-09T14:07:10.134Z",
    "currency": "USD",
    "customer_id": "cu_1736431630",
    "customer": {
        "id": "cu_1736431630",
        "name": "John Doe",
        "email": "guest@example.com",
        "phone": "999999999",
        "phone_country_code": "+1"
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "payment_checks": {
                "cvc_check": "pass",
                "address_line1_check": "pass",
                "address_postal_code_check": "pass"
            },
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": null
        },
        "phone": null,
        "email": null
    },
    "billing": {
        "address": {
            "city": "San",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": "ss"
        },
        "phone": null,
        "email": "raj@gmail.com"
    },
    "order_details": null,
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "http://127.0.0.1:4040/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": null,
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": {
        "customer_id": "cu_1736431630",
        "created_at": 1736431630,
        "expires": 1736435230,
        "secret": "epk_bf70a02d2bb64f5c81712679dedac0aa"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "pi_3QfMOBD5R7gDAGff0l6jmTHK",
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pi_3QfMOBD5R7gDAGff0l6jmTHK",
    "payment_link": null,
    "profile_id": "pro_GhoiAnNpnyEAqAAbwULW",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-01-09T14:22:10.134Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "125.0.0.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "java_script_enabled": true
    },
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2025-01-09T14:07:12.259Z",
    "split_payments": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}
```
-> Manually set the status to pending  in the db to test webhooks flow
```
curl --location 'http://localhost:8080/payments/pay_bnl7rTJB6WUbfsQRTRU3' \
--header 'Accept: application/json' \
--header 'api-key:'
```
```
{
    "payment_id": "pay_bnl7rTJB6WUbfsQRTRU3",
    "merchant_id": "merchant_1736429319",
    "status": "processing",
    "amount": 100,
    "net_amount": 100,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 100,
    "connector": "stripe",
    "client_secret": "pay_bnl7rTJB6WUbfsQRTRU3_secret_YRCZCEKKcDxD6VnbtBGl",
    "created": "2025-01-09T14:07:10.134Z",
    "currency": "USD",
    "customer_id": "cu_1736431630",
    "customer": {
        "id": "cu_1736431630",
        "name": "John Doe",
        "email": "guest@example.com",
        "phone": "999999999",
        "phone_country_code": "+1"
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "payment_checks": {
                "cvc_check": "pass",
                "address_line1_check": "pass",
                "address_postal_code_check": "pass"
            },
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": null
        },
        "phone": null,
        "email": null
    },
    "billing": {
        "address": {
            "city": "San",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": "ss"
        },
        "phone": null,
        "email": "raj@gmail.com"
    },
    "order_details": null,
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "http://127.0.0.1:4040/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": null,
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "pi_3QfMOBD5R7gDAGff0l6jmTHK",
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pi_3QfMOBD5R7gDAGff0l6jmTHK",
    "payment_link": null,
    "profile_id": "pro_GhoiAnNpnyEAqAAbwULW",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-01-09T14:22:10.134Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "125.0.0.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "java_script_enabled": true
    },
    "payment_method_id": "pm_2aJAicSBjlanyP8w6jYQ",
    "payment_method_status": "active",
    "updated": "2025-01-09T14:15:56.696Z",
    "split_payments": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}
```
-> resend the webhook and when retrieved the payment status is expected to be succeeded
```
curl --location 'http://localhost:8080/payments/pay_bnl7rTJB6WUbfsQRTRU3' \
--header 'Accept: application/json' \
--header 'api-key:'
```
```
{
    "payment_id": "pay_bnl7rTJB6WUbfsQRTRU3",
    "merchant_id": "merchant_1736429319",
    "status": "succeeded",
    "amount": 100,
    "net_amount": 100,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 100,
    "connector": "stripe",
    "client_secret": "pay_bnl7rTJB6WUbfsQRTRU3_secret_YRCZCEKKcDxD6VnbtBGl",
    "created": "2025-01-09T14:07:10.134Z",
    "currency": "USD",
    "customer_id": "cu_1736431630",
    "customer": {
        "id": "cu_1736431630",
        "name": "John Doe",
        "email": "guest@example.com",
        "phone": "999999999",
        "phone_country_code": "+1"
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "payment_checks": {
                "cvc_check": "pass",
                "address_line1_check": "pass",
                "address_postal_code_check": "pass"
            },
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": null
        },
        "phone": null,
        "email": null
    },
    "billing": {
        "address": {
            "city": "San",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "PiX",
            "last_name": "ss"
        },
        "phone": null,
        "email": "raj@gmail.com"
    },
    "order_details": null,
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "http://127.0.0.1:4040/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": null,
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "pi_3QfMOBD5R7gDAGff0l6jmTHK",
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pi_3QfMOBD5R7gDAGff0l6jmTHK",
    "payment_link": null,
    "profile_id": "pro_GhoiAnNpnyEAqAAbwULW",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_YhBV1Nr6PXdkkG7md9ro",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-01-09T14:22:10.134Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "125.0.0.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "java_script_enabled": true
    },
    "payment_method_id": "pm_2aJAicSBjlanyP8w6jYQ",
    "payment_method_status": "active",
    "updated": "2025-01-09T14:21:04.293Z",
    "split_payments": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}
```



## 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
