Bug: [FEATURE] add unit tests for non 3DS, wallets & webhooks in connector tests



### Feature Description

Unit tests should be added in the connector tests for cards Non 3DS, Wallets & Webhooks.

### Possible Implementation

Unit tests for cards Non 3ds, Wallets & webhooks should be added to the template file connector-template/test.rs. Tests should be added for already integrated connectors for Non 3ds, Wallets & webhooks flows.

### Have you spent some time to check if this feature request has been raised before?

- [X] I checked and didn't find 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?

None
diff --git a/Cargo.lock b/Cargo.lock
index 94d137885dc..40c8891825e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -226,7 +226,7 @@ dependencies = [
  "serde_urlencoded",
  "smallvec",
  "socket2",
- "time",
+ "time 0.3.20",
  "url",
 ]
 
@@ -338,7 +338,7 @@ dependencies = [
  "serde",
  "serde_json",
  "strum",
- "time",
+ "time 0.3.20",
  "url",
  "utoipa",
 ]
@@ -550,7 +550,7 @@ dependencies = [
  "http",
  "hyper",
  "ring",
- "time",
+ "time 0.3.20",
  "tokio",
  "tower",
  "tracing",
@@ -709,7 +709,7 @@ dependencies = [
  "percent-encoding",
  "regex",
  "sha2",
- "time",
+ "time 0.3.20",
  "tracing",
 ]
 
@@ -816,7 +816,7 @@ dependencies = [
  "itoa",
  "num-integer",
  "ryu",
- "time",
+ "time 0.3.20",
 ]
 
 [[package]]
@@ -1099,9 +1099,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
 dependencies = [
  "iana-time-zone",
+ "js-sys",
  "num-integer",
  "num-traits",
  "serde",
+ "time 0.1.43",
+ "wasm-bindgen",
  "winapi",
 ]
 
@@ -1191,7 +1194,7 @@ dependencies = [
  "signal-hook",
  "signal-hook-tokio",
  "thiserror",
- "time",
+ "time 0.3.20",
  "tokio",
 ]
 
@@ -1242,7 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
 dependencies = [
  "percent-encoding",
- "time",
+ "time 0.3.20",
  "version_check",
 ]
 
@@ -1482,7 +1485,7 @@ dependencies = [
  "pq-sys",
  "r2d2",
  "serde_json",
- "time",
+ "time 0.3.20",
 ]
 
 [[package]]
@@ -1662,6 +1665,28 @@ dependencies = [
  "rand 0.8.5",
 ]
 
+[[package]]
+name = "fantoccini"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64"
+dependencies = [
+ "base64 0.13.1",
+ "cookie",
+ "futures-core",
+ "futures-util",
+ "http",
+ "hyper",
+ "hyper-rustls",
+ "mime",
+ "serde",
+ "serde_json",
+ "time 0.3.20",
+ "tokio",
+ "url",
+ "webdriver",
+]
+
 [[package]]
 name = "fastrand"
 version = "1.9.0"
@@ -2308,7 +2333,7 @@ dependencies = [
  "serde",
  "serde_json",
  "thiserror",
- "time",
+ "time 0.3.20",
 ]
 
 [[package]]
@@ -3486,8 +3511,9 @@ dependencies = [
  "signal-hook-tokio",
  "storage_models",
  "strum",
+ "thirtyfour",
  "thiserror",
- "time",
+ "time 0.3.20",
  "tokio",
  "toml 0.7.3",
  "url",
@@ -3526,7 +3552,7 @@ dependencies = [
  "serde_json",
  "serde_path_to_error",
  "strum",
- "time",
+ "time 0.3.20",
  "tokio",
  "tracing",
  "tracing-actix-web",
@@ -3812,6 +3838,17 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "serde_repr"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.11",
+]
+
 [[package]]
 name = "serde_spanned"
 version = "0.6.1"
@@ -3846,7 +3883,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_with_macros",
- "time",
+ "time 0.3.20",
 ]
 
 [[package]]
@@ -3977,7 +4014,7 @@ dependencies = [
  "num-bigint",
  "num-traits",
  "thiserror",
- "time",
+ "time 0.3.20",
 ]
 
 [[package]]
@@ -4046,7 +4083,16 @@ dependencies = [
  "serde_json",
  "strum",
  "thiserror",
- "time",
+ "time 0.3.20",
+]
+
+[[package]]
+name = "stringmatch"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aadc0801d92f0cdc26127c67c4b8766284f52a5ba22894f285e3101fa57d05d"
+dependencies = [
+ "regex",
 ]
 
 [[package]]
@@ -4139,6 +4185,44 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "thirtyfour"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72fc70ad9624071cdd96d034676b84b504bfeb4bee1580df1324c99373ea0ca7"
+dependencies = [
+ "async-trait",
+ "base64 0.13.1",
+ "chrono",
+ "cookie",
+ "fantoccini",
+ "futures",
+ "http",
+ "log",
+ "parking_lot",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "stringmatch",
+ "thirtyfour-macros",
+ "thiserror",
+ "tokio",
+ "url",
+ "urlparse",
+]
+
+[[package]]
+name = "thirtyfour-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cae91d1c7c61ec65817f1064954640ee350a50ae6548ff9a1bdd2489d6ffbb0"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.40"
@@ -4169,6 +4253,16 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "time"
 version = "0.3.20"
@@ -4438,7 +4532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e"
 dependencies = [
  "crossbeam-channel",
- "time",
+ "time 0.3.20",
  "tracing-subscriber",
 ]
 
@@ -4589,6 +4683,12 @@ dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "unicode-segmentation"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+
 [[package]]
 name = "unicode-width"
 version = "0.1.10"
@@ -4619,6 +4719,12 @@ version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
 
+[[package]]
+name = "urlparse"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517"
+
 [[package]]
 name = "utoipa"
 version = "3.3.0"
@@ -4691,7 +4797,7 @@ dependencies = [
  "git2",
  "rustc_version",
  "rustversion",
- "time",
+ "time 0.3.20",
 ]
 
 [[package]]
@@ -4829,6 +4935,25 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "webdriver"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f"
+dependencies = [
+ "base64 0.13.1",
+ "bytes",
+ "cookie",
+ "http",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time 0.3.20",
+ "unicode-segmentation",
+ "url",
+]
+
 [[package]]
 name = "webpki"
 version = "0.22.0"
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index d0165aa1f13..3cd872522de 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -628,6 +628,7 @@ pub enum WalletData {
 }
 
 #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
+#[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))]
 pub struct GooglePayWalletData {
     /// The type of payment method
     #[serde(rename = "type")]
@@ -659,6 +660,7 @@ pub struct MbWayRedirection {
 }
 
 #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
+#[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))]
 pub struct GooglePayPaymentMethodInfo {
     /// The name of the card network
     pub card_network: String,
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index be584c7722d..70ae4c33e4e 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -102,6 +102,7 @@ time = { version = "0.3.20", features = ["macros"] }
 tokio = "1.27.0"
 toml = "0.7.3"
 wiremock = "0.5"
+thirtyfour = "0.31.0"
 
 [[bin]]
 name = "router"
diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs
index 27cb1b7f779..576c8cece64 100644
--- a/crates/router/src/connector/nuvei.rs
+++ b/crates/router/src/connector/nuvei.rs
@@ -10,7 +10,7 @@ use ::common_utils::{
 use error_stack::{IntoReport, ResultExt};
 use transformers as nuvei;
 
-use super::utils::{self, to_boolean, RouterData};
+use super::utils::{self, RouterData};
 use crate::{
     configs::settings,
     core::{
@@ -486,35 +486,42 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
         )
         .await?;
         router_data.session_token = resp.session_token;
-        let (enrolled_for_3ds, related_transaction_id) = match router_data.auth_type {
-            storage_models::enums::AuthenticationType::ThreeDs => {
-                let integ: Box<
-                    &(dyn ConnectorIntegration<
-                        InitPayment,
-                        types::PaymentsAuthorizeData,
-                        types::PaymentsResponseData,
-                    > + Send
-                          + Sync
-                          + 'static),
-                > = Box::new(&Self);
-                let init_data = &types::PaymentsInitRouterData::from((
-                    &router_data,
-                    router_data.request.clone(),
-                ));
-                let init_resp = services::execute_connector_processing_step(
-                    app_state,
-                    integ,
-                    init_data,
-                    payments::CallConnectorAction::Trigger,
-                )
-                .await?;
+        let (enrolled_for_3ds, related_transaction_id) =
+            match (router_data.auth_type, router_data.payment_method) {
                 (
-                    init_resp.request.enrolled_for_3ds,
-                    init_resp.request.related_transaction_id,
-                )
-            }
-            storage_models::enums::AuthenticationType::NoThreeDs => (false, None),
-        };
+                    storage_models::enums::AuthenticationType::ThreeDs,
+                    storage_models::enums::PaymentMethod::Card,
+                ) => {
+                    let integ: Box<
+                        &(dyn ConnectorIntegration<
+                            InitPayment,
+                            types::PaymentsAuthorizeData,
+                            types::PaymentsResponseData,
+                        > + Send
+                              + Sync
+                              + 'static),
+                    > = Box::new(&Self);
+                    let init_data = &types::PaymentsInitRouterData::from((
+                        &router_data,
+                        router_data.request.clone(),
+                    ));
+                    let init_resp = services::execute_connector_processing_step(
+                        app_state,
+                        integ,
+                        init_data,
+                        payments::CallConnectorAction::Trigger,
+                    )
+                    .await?;
+                    match init_resp.response {
+                        Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse {
+                            enrolled_v2,
+                            related_transaction_id,
+                        }) => (enrolled_v2, related_transaction_id),
+                        _ => (false, None),
+                    }
+                }
+                _ => (false, None),
+            };
 
         router_data.request.enrolled_for_3ds = enrolled_for_3ds;
         router_data.request.related_transaction_id = related_transaction_id;
@@ -725,28 +732,12 @@ impl ConnectorIntegration<InitPayment, types::PaymentsAuthorizeData, types::Paym
             .response
             .parse_struct("NuveiPaymentsResponse")
             .switch()?;
-        let response_data = types::RouterData::try_from(types::ResponseRouterData {
-            response: response.clone(),
+        types::RouterData::try_from(types::ResponseRouterData {
+            response,
             data: data.clone(),
             http_code: res.status_code,
         })
-        .change_context(errors::ConnectorError::ResponseHandlingFailed)?;
-        let is_enrolled_for_3ds = response
-            .clone()
-            .payment_option
-            .and_then(|po| po.card)
-            .and_then(|c| c.three_d)
-            .and_then(|t| t.v2supported)
-            .map(to_boolean)
-            .unwrap_or_default();
-        Ok(types::RouterData {
-            request: types::PaymentsAuthorizeData {
-                enrolled_for_3ds: is_enrolled_for_3ds,
-                related_transaction_id: response.transaction_id,
-                ..response_data.request
-            },
-            ..response_data
-        })
+        .change_context(errors::ConnectorError::ResponseHandlingFailed)
     }
 
     fn get_error_response(
diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs
index d554bf74be9..2d125818b70 100644
--- a/crates/router/src/connector/nuvei/transformers.rs
+++ b/crates/router/src/connector/nuvei/transformers.rs
@@ -1,3 +1,4 @@
+use api_models::payments;
 use common_utils::{
     crypto::{self, GenerateDigest},
     date_time, fp_utils,
@@ -10,12 +11,13 @@ use serde::{Deserialize, Serialize};
 
 use crate::{
     connector::utils::{
-        self, MandateData, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, RouterData,
+        self, AddressDetailsData, MandateData, PaymentsAuthorizeRequestData,
+        PaymentsCancelRequestData, RouterData,
     },
     consts,
     core::errors,
     services,
-    types::{self, api, storage::enums},
+    types::{self, api, storage::enums, transformers::ForeignTryFrom},
 };
 
 #[derive(Debug, Serialize, Default, Deserialize)]
@@ -62,7 +64,7 @@ pub struct NuveiPaymentsRequest {
     pub merchant_site_id: String,
     pub client_request_id: String,
     pub amount: String,
-    pub currency: String,
+    pub currency: storage_models::enums::Currency,
     /// This ID uniquely identifies your consumer/user in your system.
     pub user_token_id: Option<Secret<String, Email>>,
     pub client_unique_id: String,
@@ -95,7 +97,7 @@ pub struct NuveiPaymentFlowRequest {
     pub merchant_site_id: String,
     pub client_request_id: String,
     pub amount: String,
-    pub currency: String,
+    pub currency: storage_models::enums::Currency,
     pub related_transaction_id: Option<String>,
     pub checksum: String,
 }
@@ -125,22 +127,65 @@ pub struct PaymentOption {
     pub billing_address: Option<BillingAddress>,
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum NuveiBIC {
+    #[serde(rename = "ABNANL2A")]
+    Abnamro,
+    #[serde(rename = "ASNBNL21")]
+    ASNBank,
+    #[serde(rename = "BUNQNL2A")]
+    Bunq,
+    #[serde(rename = "INGBNL2A")]
+    Ing,
+    #[serde(rename = "KNABNL2H")]
+    Knab,
+    #[serde(rename = "RABONL2U")]
+    Rabobank,
+    #[serde(rename = "RBRBNL21")]
+    RegioBank,
+    #[serde(rename = "SNSBNL2A")]
+    SNSBank,
+    #[serde(rename = "TRIONL2U")]
+    TriodosBank,
+    #[serde(rename = "FVLBNL22")]
+    VanLanschotBankiers,
+    #[serde(rename = "MOYONL21")]
+    Moneyou,
+}
+
+#[serde_with::skip_serializing_none]
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct AlternativePaymentMethod {
     pub payment_method: AlternativePaymentMethodType,
+    #[serde(rename = "BIC")]
+    pub bank_id: Option<NuveiBIC>,
 }
 
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub enum AlternativePaymentMethodType {
     #[default]
-    ApmgwExpresscheckout,
+    #[serde(rename = "apmgw_expresscheckout")]
+    Expresscheckout,
+    #[serde(rename = "apmgw_Giropay")]
+    Giropay,
+    #[serde(rename = "apmgw_Sofort")]
+    Sofort,
+    #[serde(rename = "apmgw_iDeal")]
+    Ideal,
+    #[serde(rename = "apmgw_EPS")]
+    Eps,
 }
 
+#[serde_with::skip_serializing_none]
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
 pub struct BillingAddress {
-    pub email: Secret<String, Email>,
+    pub email: Option<Secret<String, Email>>,
+    pub first_name: Option<Secret<String>>,
+    pub last_name: Option<Secret<String>>,
     pub country: api_models::enums::CountryCode,
 }
 
@@ -366,28 +411,34 @@ impl<F, T>
 
 #[derive(Debug, Default)]
 pub struct NuveiCardDetails {
-    card: api_models::payments::Card,
+    card: payments::Card,
     three_d: Option<ThreeD>,
 }
-impl From<api_models::payments::GooglePayWalletData> for NuveiPaymentsRequest {
-    fn from(gpay_data: api_models::payments::GooglePayWalletData) -> Self {
-        Self {
+impl TryFrom<payments::GooglePayWalletData> for NuveiPaymentsRequest {
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn try_from(gpay_data: payments::GooglePayWalletData) -> Result<Self, Self::Error> {
+        Ok(Self {
             payment_option: PaymentOption {
                 card: Some(Card {
                     external_token: Some(ExternalToken {
                         external_token_provider: ExternalTokenProvider::GooglePay,
-                        mobile_token: gpay_data.tokenization_data.token,
+                        mobile_token: common_utils::ext_traits::Encode::<
+                            payments::GooglePayWalletData,
+                        >::encode_to_string_of_json(
+                            &gpay_data
+                        )
+                        .change_context(errors::ConnectorError::RequestEncodingFailed)?,
                     }),
                     ..Default::default()
                 }),
                 ..Default::default()
             },
             ..Default::default()
-        }
+        })
     }
 }
-impl From<api_models::payments::ApplePayWalletData> for NuveiPaymentsRequest {
-    fn from(apple_pay_data: api_models::payments::ApplePayWalletData) -> Self {
+impl From<payments::ApplePayWalletData> for NuveiPaymentsRequest {
+    fn from(apple_pay_data: payments::ApplePayWalletData) -> Self {
         Self {
             payment_option: PaymentOption {
                 card: Some(Card {
@@ -404,6 +455,109 @@ impl From<api_models::payments::ApplePayWalletData> for NuveiPaymentsRequest {
     }
 }
 
+impl TryFrom<api_models::enums::BankNames> for NuveiBIC {
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn try_from(bank: api_models::enums::BankNames) -> Result<Self, Self::Error> {
+        match bank {
+            api_models::enums::BankNames::AbnAmro => Ok(Self::Abnamro),
+            api_models::enums::BankNames::AsnBank => Ok(Self::ASNBank),
+            api_models::enums::BankNames::Bunq => Ok(Self::Bunq),
+            api_models::enums::BankNames::Ing => Ok(Self::Ing),
+            api_models::enums::BankNames::Knab => Ok(Self::Knab),
+            api_models::enums::BankNames::Rabobank => Ok(Self::Rabobank),
+            api_models::enums::BankNames::SnsBank => Ok(Self::SNSBank),
+            api_models::enums::BankNames::TriodosBank => Ok(Self::TriodosBank),
+            api_models::enums::BankNames::VanLanschot => Ok(Self::VanLanschotBankiers),
+            api_models::enums::BankNames::Moneyou => Ok(Self::Moneyou),
+            _ => Err(errors::ConnectorError::FlowNotSupported {
+                flow: bank.to_string(),
+                connector: "Nuvei".to_string(),
+            }
+            .into()),
+        }
+    }
+}
+
+impl<F>
+    ForeignTryFrom<(
+        AlternativePaymentMethodType,
+        Option<payments::BankRedirectData>,
+        &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
+    )> for NuveiPaymentsRequest
+{
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn foreign_try_from(
+        data: (
+            AlternativePaymentMethodType,
+            Option<payments::BankRedirectData>,
+            &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
+        ),
+    ) -> Result<Self, Self::Error> {
+        let (payment_method, redirect, item) = data;
+        let (billing_address, bank_id) = match (&payment_method, redirect) {
+            (AlternativePaymentMethodType::Expresscheckout, _) => (
+                Some(BillingAddress {
+                    email: Some(item.request.get_email()?),
+                    country: item.get_billing_country()?,
+                    ..Default::default()
+                }),
+                None,
+            ),
+            (AlternativePaymentMethodType::Giropay, _) => (
+                Some(BillingAddress {
+                    email: Some(item.request.get_email()?),
+                    country: item.get_billing_country()?,
+                    ..Default::default()
+                }),
+                None,
+            ),
+            (AlternativePaymentMethodType::Sofort, _) | (AlternativePaymentMethodType::Eps, _) => {
+                let address = item.get_billing_address()?;
+                (
+                    Some(BillingAddress {
+                        first_name: Some(address.get_first_name()?.clone()),
+                        last_name: Some(address.get_last_name()?.clone()),
+                        email: Some(item.request.get_email()?),
+                        country: item.get_billing_country()?,
+                    }),
+                    None,
+                )
+            }
+            (
+                AlternativePaymentMethodType::Ideal,
+                Some(payments::BankRedirectData::Ideal { bank_name, .. }),
+            ) => {
+                let address = item.get_billing_address()?;
+                (
+                    Some(BillingAddress {
+                        first_name: Some(address.get_first_name()?.clone()),
+                        last_name: Some(address.get_last_name()?.clone()),
+                        email: Some(item.request.get_email()?),
+                        country: item.get_billing_country()?,
+                    }),
+                    Some(NuveiBIC::try_from(bank_name)?),
+                )
+            }
+            _ => Err(errors::ConnectorError::NotSupported {
+                payment_method: "Bank Redirect".to_string(),
+                connector: "Nuvei",
+                payment_experience: "Redirection".to_string(),
+            })?,
+        };
+        Ok(Self {
+            payment_option: PaymentOption {
+                alternative_payment_method: Some(AlternativePaymentMethod {
+                    payment_method,
+                    bank_id,
+                }),
+                ..Default::default()
+            },
+            billing_address,
+            ..Default::default()
+        })
+    }
+}
+
 impl<F>
     TryFrom<(
         &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
@@ -421,23 +575,13 @@ impl<F>
         let request_data = match item.request.payment_method_data.clone() {
             api::PaymentMethodData::Card(card) => get_card_info(item, &card),
             api::PaymentMethodData::Wallet(wallet) => match wallet {
-                api_models::payments::WalletData::GooglePay(gpay_data) => Ok(Self::from(gpay_data)),
-                api_models::payments::WalletData::ApplePay(apple_pay_data) => {
-                    Ok(Self::from(apple_pay_data))
-                }
-                api_models::payments::WalletData::PaypalRedirect(_) => Ok(Self {
-                    payment_option: PaymentOption {
-                        alternative_payment_method: Some(AlternativePaymentMethod {
-                            payment_method: AlternativePaymentMethodType::ApmgwExpresscheckout,
-                        }),
-                        ..Default::default()
-                    },
-                    billing_address: Some(BillingAddress {
-                        email: item.request.get_email()?,
-                        country: item.get_billing_country()?,
-                    }),
-                    ..Default::default()
-                }),
+                payments::WalletData::GooglePay(gpay_data) => Self::try_from(gpay_data),
+                payments::WalletData::ApplePay(apple_pay_data) => Ok(Self::from(apple_pay_data)),
+                payments::WalletData::PaypalRedirect(_) => Self::foreign_try_from((
+                    AlternativePaymentMethodType::Expresscheckout,
+                    None,
+                    item,
+                )),
                 _ => Err(errors::ConnectorError::NotSupported {
                     payment_method: "Wallet".to_string(),
                     connector: "Nuvei",
@@ -445,11 +589,39 @@ impl<F>
                 }
                 .into()),
             },
+            api::PaymentMethodData::BankRedirect(redirect) => match redirect {
+                payments::BankRedirectData::Eps { .. } => Self::foreign_try_from((
+                    AlternativePaymentMethodType::Eps,
+                    Some(redirect),
+                    item,
+                )),
+                payments::BankRedirectData::Giropay { .. } => Self::foreign_try_from((
+                    AlternativePaymentMethodType::Giropay,
+                    Some(redirect),
+                    item,
+                )),
+                payments::BankRedirectData::Ideal { .. } => Self::foreign_try_from((
+                    AlternativePaymentMethodType::Ideal,
+                    Some(redirect),
+                    item,
+                )),
+                payments::BankRedirectData::Sofort { .. } => Self::foreign_try_from((
+                    AlternativePaymentMethodType::Sofort,
+                    Some(redirect),
+                    item,
+                )),
+                _ => Err(errors::ConnectorError::NotSupported {
+                    payment_method: "Bank Redirect".to_string(),
+                    connector: "Nuvei",
+                    payment_experience: "RedirectToUrl".to_string(),
+                }
+                .into()),
+            },
             _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
         }?;
         let request = Self::try_from(NuveiPaymentRequestData {
-            amount: item.request.amount.clone().to_string(),
-            currency: item.request.currency.clone().to_string(),
+            amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
+            currency: item.request.currency,
             connector_auth_type: item.connector_auth_type.clone(),
             client_request_id: item.attempt_id.clone(),
             session_token: data.1,
@@ -461,6 +633,7 @@ impl<F>
             user_token_id: request_data.user_token_id,
             related_transaction_id: request_data.related_transaction_id,
             payment_option: request_data.payment_option,
+            billing_address: request_data.billing_address,
             ..request
         })
     }
@@ -468,7 +641,7 @@ impl<F>
 
 fn get_card_info<F>(
     item: &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
-    card_details: &api_models::payments::Card,
+    card_details: &payments::Card,
 ) -> Result<NuveiPaymentsRequest, error_stack::Report<errors::ConnectorError>> {
     let browser_info = item.request.get_browser_info()?;
     let related_transaction_id = if item.is_three_ds() {
@@ -493,8 +666,8 @@ fn get_card_info<F>(
             match item.request.setup_mandate_details.clone() {
                 Some(mandate_data) => {
                     let details = match mandate_data.mandate_type {
-                        api_models::payments::MandateType::SingleUse(details) => details,
-                        api_models::payments::MandateType::MultiUse(details) => {
+                        payments::MandateType::SingleUse(details) => details,
+                        payments::MandateType::MultiUse(details) => {
                             details.ok_or(errors::ConnectorError::MissingRequiredField {
                                 field_name: "mandate_data.mandate_type.multi_use",
                             })?
@@ -593,8 +766,8 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPay
             )),
         }?;
         let request = Self::try_from(NuveiPaymentRequestData {
-            amount: item.request.amount.clone().to_string(),
-            currency: item.request.currency.clone().to_string(),
+            amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
+            currency: item.request.currency,
             connector_auth_type: item.connector_auth_type.clone(),
             client_request_id: item.attempt_id.clone(),
             session_token: data.1,
@@ -640,7 +813,7 @@ impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentsRequest {
                 merchant_site_id,
                 client_request_id,
                 request.amount.clone(),
-                request.currency.clone(),
+                request.currency.clone().to_string(),
                 time_stamp,
                 merchant_secret,
             ])?,
@@ -673,7 +846,7 @@ impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentFlowRequest {
                 merchant_site_id,
                 client_request_id,
                 request.amount.clone(),
-                request.currency.clone(),
+                request.currency.clone().to_string(),
                 request.related_transaction_id.clone().unwrap_or_default(),
                 time_stamp,
                 merchant_secret,
@@ -685,11 +858,10 @@ impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentFlowRequest {
     }
 }
 
-/// Common request handler for all the flows that has below fields in common
 #[derive(Debug, Clone, Default)]
 pub struct NuveiPaymentRequestData {
     pub amount: String,
-    pub currency: String,
+    pub currency: storage_models::enums::Currency,
     pub related_transaction_id: Option<String>,
     pub client_request_id: String,
     pub connector_auth_type: types::ConnectorAuthType,
@@ -704,7 +876,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NuveiPaymentFlowRequest {
             client_request_id: item.attempt_id.clone(),
             connector_auth_type: item.connector_auth_type.clone(),
             amount: item.request.amount_to_capture.to_string(),
-            currency: item.request.currency.to_string(),
+            currency: item.request.currency,
             related_transaction_id: Some(item.request.connector_transaction_id.clone()),
             ..Default::default()
         })
@@ -717,7 +889,7 @@ impl TryFrom<&types::RefundExecuteRouterData> for NuveiPaymentFlowRequest {
             client_request_id: item.attempt_id.clone(),
             connector_auth_type: item.connector_auth_type.clone(),
             amount: item.request.amount.to_string(),
-            currency: item.request.currency.to_string(),
+            currency: item.request.currency,
             related_transaction_id: Some(item.request.connector_transaction_id.clone()),
             ..Default::default()
         })
@@ -741,7 +913,7 @@ impl TryFrom<&types::PaymentsCancelRouterData> for NuveiPaymentFlowRequest {
             client_request_id: item.attempt_id.clone(),
             connector_auth_type: item.connector_auth_type.clone(),
             amount: item.request.get_amount()?.to_string(),
-            currency: item.request.get_currency()?.to_string(),
+            currency: item.request.get_currency()?,
             related_transaction_id: Some(item.request.connector_transaction_id.clone()),
             ..Default::default()
         })
@@ -868,14 +1040,11 @@ fn get_payment_status(response: &NuveiPaymentsResponse) -> enums::AttemptStatus
             NuveiTransactionStatus::Declined | NuveiTransactionStatus::Error => {
                 match response.transaction_type {
                     Some(NuveiTransactionType::Auth) => enums::AttemptStatus::AuthorizationFailed,
-                    Some(NuveiTransactionType::Sale) | Some(NuveiTransactionType::Settle) => {
-                        enums::AttemptStatus::Failure
-                    }
                     Some(NuveiTransactionType::Void) => enums::AttemptStatus::VoidFailed,
                     Some(NuveiTransactionType::Auth3D) => {
                         enums::AttemptStatus::AuthenticationFailed
                     }
-                    _ => enums::AttemptStatus::Pending,
+                    _ => enums::AttemptStatus::Failure,
                 }
             }
             NuveiTransactionStatus::Processing => enums::AttemptStatus::Pending,
@@ -888,16 +1057,58 @@ fn get_payment_status(response: &NuveiPaymentsResponse) -> enums::AttemptStatus
     }
 }
 
+fn build_error_response<T>(
+    response: &NuveiPaymentsResponse,
+    http_code: u16,
+) -> Option<Result<T, types::ErrorResponse>> {
+    match response.status {
+        NuveiPaymentStatus::Error => Some(get_error_response(
+            response.err_code,
+            &response.reason,
+            http_code,
+        )),
+        _ => {
+            let err = Some(get_error_response(
+                response.gw_error_code,
+                &response.gw_error_reason,
+                http_code,
+            ));
+            match response.transaction_status {
+                Some(NuveiTransactionStatus::Error) => err,
+                _ => match response
+                    .gw_error_reason
+                    .as_ref()
+                    .map(|r| r.eq("Missing argument"))
+                {
+                    Some(true) => err,
+                    _ => None,
+                },
+            }
+        }
+    }
+}
+
+pub trait NuveiPaymentsGenericResponse {}
+
+impl NuveiPaymentsGenericResponse for api::Authorize {}
+impl NuveiPaymentsGenericResponse for api::CompleteAuthorize {}
+impl NuveiPaymentsGenericResponse for api::Void {}
+impl NuveiPaymentsGenericResponse for api::PSync {}
+impl NuveiPaymentsGenericResponse for api::Capture {}
+
 impl<F, T>
     TryFrom<types::ResponseRouterData<F, NuveiPaymentsResponse, T, types::PaymentsResponseData>>
     for types::RouterData<F, T, types::PaymentsResponseData>
+where
+    F: NuveiPaymentsGenericResponse,
 {
     type Error = error_stack::Report<errors::ConnectorError>;
     fn try_from(
         item: types::ResponseRouterData<F, NuveiPaymentsResponse, T, types::PaymentsResponseData>,
     ) -> Result<Self, Self::Error> {
         let redirection_data = match item.data.payment_method {
-            storage_models::enums::PaymentMethod::Wallet => item
+            storage_models::enums::PaymentMethod::Wallet
+            | storage_models::enums::PaymentMethod::BankRedirect => item
                 .response
                 .payment_option
                 .as_ref()
@@ -920,46 +1131,65 @@ impl<F, T>
         let response = item.response;
         Ok(Self {
             status: get_payment_status(&response),
-            response: match response.status {
-                NuveiPaymentStatus::Error => {
-                    get_error_response(response.err_code, response.reason, item.http_code)
-                }
-                _ => match response.transaction_status {
-                    Some(NuveiTransactionStatus::Error) => get_error_response(
-                        response.gw_error_code,
-                        response.gw_error_reason,
-                        item.http_code,
-                    ),
-                    _ => Ok(types::PaymentsResponseData::TransactionResponse {
-                        resource_id: response
-                            .transaction_id
-                            .map_or(response.order_id, Some) // For paypal there will be no transaction_id, only order_id will be present
-                            .map(types::ResponseId::ConnectorTransactionId)
-                            .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?,
-                        redirection_data,
-                        mandate_reference: response
-                            .payment_option
-                            .and_then(|po| po.user_payment_option_id),
-                        // we don't need to save session token for capture, void flow so ignoring if it is not present
-                        connector_metadata: if let Some(token) = response.session_token {
-                            Some(
-                                serde_json::to_value(NuveiMeta {
-                                    session_token: token,
-                                })
-                                .into_report()
-                                .change_context(errors::ConnectorError::ResponseHandlingFailed)?,
-                            )
-                        } else {
-                            None
-                        },
-                    }),
-                },
+            response: if let Some(err) = build_error_response(&response, item.http_code) {
+                err
+            } else {
+                Ok(types::PaymentsResponseData::TransactionResponse {
+                    resource_id: response
+                        .transaction_id
+                        .map_or(response.order_id, Some) // For paypal there will be no transaction_id, only order_id will be present
+                        .map(types::ResponseId::ConnectorTransactionId)
+                        .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?,
+                    redirection_data,
+                    mandate_reference: response
+                        .payment_option
+                        .and_then(|po| po.user_payment_option_id),
+                    // we don't need to save session token for capture, void flow so ignoring if it is not present
+                    connector_metadata: if let Some(token) = response.session_token {
+                        Some(
+                            serde_json::to_value(NuveiMeta {
+                                session_token: token,
+                            })
+                            .into_report()
+                            .change_context(errors::ConnectorError::ResponseHandlingFailed)?,
+                        )
+                    } else {
+                        None
+                    },
+                })
             },
             ..item.data
         })
     }
 }
 
+impl TryFrom<types::PaymentsInitResponseRouterData<NuveiPaymentsResponse>>
+    for types::PaymentsInitRouterData
+{
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn try_from(
+        item: types::PaymentsInitResponseRouterData<NuveiPaymentsResponse>,
+    ) -> Result<Self, Self::Error> {
+        let response = item.response;
+        let is_enrolled_for_3ds = response
+            .clone()
+            .payment_option
+            .and_then(|po| po.card)
+            .and_then(|c| c.three_d)
+            .and_then(|t| t.v2supported)
+            .map(utils::to_boolean)
+            .unwrap_or_default();
+        Ok(Self {
+            status: get_payment_status(&response),
+            response: Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse {
+                enrolled_v2: is_enrolled_for_3ds,
+                related_transaction_id: response.transaction_id,
+            }),
+            ..item.data
+        })
+    }
+}
+
 impl From<NuveiTransactionStatus> for enums::RefundStatus {
     fn from(item: NuveiTransactionStatus) -> Self {
         match item {
@@ -1022,11 +1252,11 @@ fn get_refund_response(
         .unwrap_or(enums::RefundStatus::Failure);
     match response.status {
         NuveiPaymentStatus::Error => {
-            get_error_response(response.err_code, response.reason, http_code)
+            get_error_response(response.err_code, &response.reason, http_code)
         }
         _ => match response.transaction_status {
             Some(NuveiTransactionStatus::Error) => {
-                get_error_response(response.gw_error_code, response.gw_error_reason, http_code)
+                get_error_response(response.gw_error_code, &response.gw_error_reason, http_code)
             }
             _ => Ok(types::RefundsResponseData {
                 connector_refund_id: txn_id,
@@ -1038,14 +1268,16 @@ fn get_refund_response(
 
 fn get_error_response<T>(
     error_code: Option<i64>,
-    error_msg: Option<String>,
+    error_msg: &Option<String>,
     http_code: u16,
 ) -> Result<T, types::ErrorResponse> {
     Err(types::ErrorResponse {
         code: error_code
             .map(|c| c.to_string())
             .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()),
-        message: error_msg.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
+        message: error_msg
+            .clone()
+            .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
         reason: None,
         status_code: http_code,
     })
diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs
index 9a2434fd4ef..b5432f0fb20 100644
--- a/crates/router/src/core/payments/flows/authorize_flow.rs
+++ b/crates/router/src/core/payments/flows/authorize_flow.rs
@@ -117,8 +117,10 @@ impl types::PaymentsAuthorizeRouterData {
                     .execute_pretasks(self, state)
                     .await
                     .map_err(|error| error.to_payment_failed_response())?;
+                logger::debug!(completed_pre_tasks=?true);
                 if self.should_proceed_with_authorize() {
                     self.decide_authentication_type();
+                    logger::debug!(auth_type=?self.auth_type);
                     let resp = services::execute_connector_processing_step(
                         state,
                         connector_integration,
diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs
index 7b17071ec0c..a4849b5dc81 100644
--- a/crates/router/src/core/payments/operations/payment_response.rs
+++ b/crates/router/src/core/payments/operations/payment_response.rs
@@ -387,6 +387,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
             types::PaymentsResponseData::SessionResponse { .. } => (None, None),
             types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None),
             types::PaymentsResponseData::TokenizationResponse { .. } => (None, None),
+            types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None),
         },
     };
 
diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs
index ce51c47b53f..fa7a69f7ce5 100644
--- a/crates/router/src/types.rs
+++ b/crates/router/src/types.rs
@@ -51,6 +51,8 @@ pub type PaymentsSyncResponseRouterData<R> =
     ResponseRouterData<api::PSync, R, PaymentsSyncData, PaymentsResponseData>;
 pub type PaymentsSessionResponseRouterData<R> =
     ResponseRouterData<api::Session, R, PaymentsSessionData, PaymentsResponseData>;
+pub type PaymentsInitResponseRouterData<R> =
+    ResponseRouterData<api::InitPayment, R, PaymentsAuthorizeData, PaymentsResponseData>;
 pub type PaymentsCaptureResponseRouterData<R> =
     ResponseRouterData<api::Capture, R, PaymentsCaptureData, PaymentsResponseData>;
 pub type TokenizationResponseRouterData<R> = ResponseRouterData<
@@ -283,6 +285,10 @@ pub enum PaymentsResponseData {
     TokenizationResponse {
         token: String,
     },
+    ThreeDSEnrollmentResponse {
+        enrolled_v2: bool,
+        related_transaction_id: Option<String>,
+    },
 }
 
 #[derive(Debug, Clone, Default)]
diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs
index 5d80c9f7347..55dbea1d5ca 100644
--- a/crates/router/tests/connectors/main.rs
+++ b/crates/router/tests/connectors/main.rs
@@ -18,11 +18,13 @@ mod mollie;
 mod multisafepay;
 mod nexinets;
 mod nuvei;
+mod nuvei_ui;
 mod opennode;
 mod payeezy;
 mod paypal;
 mod payu;
 mod rapyd;
+mod selenium;
 mod shift4;
 mod stripe;
 mod trustpay;
diff --git a/crates/router/tests/connectors/nuvei_ui.rs b/crates/router/tests/connectors/nuvei_ui.rs
new file mode 100644
index 00000000000..784bc7ff8b6
--- /dev/null
+++ b/crates/router/tests/connectors/nuvei_ui.rs
@@ -0,0 +1,177 @@
+use serial_test::serial;
+use thirtyfour::{prelude::*, WebDriver};
+
+use crate::{selenium::*, tester};
+
+struct NuveiSeleniumTest;
+
+impl SeleniumTest for NuveiSeleniumTest {}
+
+async fn should_make_nuvei_3ds_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_redirection_payment(c, vec![
+            Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=pm-card&cname=CL-BRW1&ccnum=4000027891380961&expmonth=10&expyear=25&cvv=123&amount=200&country=US&currency=USD")),
+            Event::Assert(Assert::IsPresent("Exp Year")),
+            Event::Trigger(Trigger::Click(By::Id("card-submit-btn"))),
+            Event::Trigger(Trigger::Query(By::ClassName("title"))),
+            Event::Assert(Assert::Eq(Selector::Title, "ThreeDS ACS Emulator - Challenge Page")),
+            Event::Trigger(Trigger::Click(By::Id("btn1"))),
+            Event::Trigger(Trigger::Click(By::Id("btn5"))),
+            Event::Assert(Assert::IsPresent("Google")),
+            Event::Assert(Assert::Contains(Selector::QueryParamStr, "status=succeeded")),
+
+    ]).await?;
+    Ok(())
+}
+
+async fn should_make_nuvei_3ds_mandate_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_redirection_payment(c, vec![
+            Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=pm-card&cname=CL-BRW1&ccnum=4000027891380961&expmonth=10&expyear=25&cvv=123&amount=200&country=US&currency=USD&setup_future_usage=off_session&mandate_data[customer_acceptance][acceptance_type]=offline&mandate_data[customer_acceptance][accepted_at]=1963-05-03T04:07:52.723Z&mandate_data[customer_acceptance][online][ip_address]=in%20sit&mandate_data[customer_acceptance][online][user_agent]=amet%20irure%20esse&mandate_data[mandate_type][multi_use][amount]=7000&mandate_data[mandate_type][multi_use][currency]=USD&mandate_data[mandate_type][multi_use][start_date]=2022-09-10T00:00:00Z&mandate_data[mandate_type][multi_use][end_date]=2023-09-10T00:00:00Z&mandate_data[mandate_type][multi_use][metadata][frequency]=13")),
+            Event::Trigger(Trigger::Click(By::Id("card-submit-btn"))),
+            Event::Trigger(Trigger::Query(By::ClassName("title"))),
+            Event::Assert(Assert::Eq(Selector::Title, "ThreeDS ACS Emulator - Challenge Page")),
+            Event::Trigger(Trigger::Click(By::Id("btn1"))),
+            Event::Trigger(Trigger::Click(By::Id("btn5"))),
+            Event::Assert(Assert::IsPresent("Google")),
+            Event::Assert(Assert::Contains(Selector::QueryParamStr, "status=succeeded")),
+
+    ]).await?;
+    Ok(())
+}
+
+async fn should_make_nuvei_gpay_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_gpay_payment(c,
+        "https://hs-payment-tests.w3spaces.com?pay-mode=pm-gpay&gatewayname=nuveidigital&gatewaymerchantid=googletest&amount=10.00&country=IN&currency=USD",
+        vec![
+        Event::Assert(Assert::IsPresent("succeeded")),
+    ]).await?;
+    Ok(())
+}
+
+async fn should_make_nuvei_pypl_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_paypal_payment(c,
+        "https://hs-payment-tests.w3spaces.com?pay-mode=pypl-redirect&amount=12.00&country=US&currency=USD",
+        vec![
+    Event::Assert(Assert::IsPresent("Your transaction has been successfully executed.")),
+    ]).await?;
+    Ok(())
+}
+
+async fn should_make_nuvei_giropay_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_redirection_payment(c, vec![
+            Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=1.00&country=DE&currency=EUR&paymentmethod=giropay")),
+            Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
+            Event::Assert(Assert::IsPresent("You are about to make a payment using the Giropay service.")),
+            Event::Trigger(Trigger::Click(By::Id("ctl00_ctl00_mainContent_btnConfirm"))),
+            Event::RunIf(Assert::IsPresent("Bank suchen"), vec![
+                Event::Trigger(Trigger::SendKeys(By::Id("bankSearch"), "GIROPAY Testbank 1")),
+                Event::Trigger(Trigger::Click(By::Id("GIROPAY Testbank 1"))),
+            ]),
+            Event::Assert(Assert::IsPresent("GIROPAY Testbank 1")),
+            Event::Trigger(Trigger::Click(By::Css("button[name='claimCheckoutButton']"))),
+            Event::Assert(Assert::IsPresent("sandbox.paydirekt")),
+            Event::Trigger(Trigger::Click(By::Id("submitButton"))),
+            Event::Trigger(Trigger::Sleep(5)),
+            Event::Trigger(Trigger::SwitchTab(Position::Next)),
+            Event::Assert(Assert::IsPresent("Sicher bezahlt!")),
+            Event::Assert(Assert::IsPresent("Your transaction")) // Transaction succeeds sometimes and pending sometimes
+    ]).await?;
+    Ok(())
+}
+
+async fn should_make_nuvei_ideal_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_redirection_payment(c, vec![
+            Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=10.00&country=NL&currency=EUR&paymentmethod=ideal&processingbank=ing")),
+            Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
+            Event::Assert(Assert::IsPresent("Your account will be debited:")),
+            Event::Trigger(Trigger::SelectOption(By::Id("ctl00_ctl00_mainContent_ServiceContent_ddlBanks"), "ING Simulator")),
+            Event::Trigger(Trigger::Click(By::Id("ctl00_ctl00_mainContent_btnConfirm"))),
+            Event::Assert(Assert::IsPresent("IDEALFORTIS")),
+            Event::Trigger(Trigger::Sleep(5)),
+            Event::Trigger(Trigger::Click(By::Id("ctl00_mainContent_btnGo"))),
+            Event::Assert(Assert::IsPresent("Your transaction")),// Transaction succeeds sometimes and pending sometimes
+    ]).await?;
+    Ok(())
+}
+
+async fn should_make_nuvei_sofort_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_redirection_payment(c, vec![
+            Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=10.00&country=DE&currency=EUR&paymentmethod=sofort")),
+            Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
+            Event::Assert(Assert::IsPresent("SOFORT")),
+            Event::Trigger(Trigger::ChangeQueryParam("sender_holder", "John Doe")),
+            Event::Trigger(Trigger::Click(By::Id("ctl00_mainContent_btnGo"))),
+            Event::Assert(Assert::IsPresent("Your transaction")),// Transaction succeeds sometimes and pending sometimes
+    ]).await?;
+    Ok(())
+}
+
+async fn should_make_nuvei_eps_payment(c: WebDriver) -> Result<(), WebDriverError> {
+    let conn = NuveiSeleniumTest {};
+    conn.make_redirection_payment(c, vec![
+            Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=10.00&country=AT&currency=EUR&paymentmethod=eps&processingbank=ing")),
+            Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
+            Event::Assert(Assert::IsPresent("You are about to make a payment using the EPS service.")),
+            Event::Trigger(Trigger::SendKeys(By::Id("ctl00_ctl00_mainContent_ServiceContent_txtCustomerName"), "John Doe")),
+            Event::Trigger(Trigger::Click(By::Id("ctl00_ctl00_mainContent_btnConfirm"))),
+            Event::Assert(Assert::IsPresent("Simulator")),
+            Event::Trigger(Trigger::SelectOption(By::Css("select[name='result']"), "Succeeded")),
+            Event::Trigger(Trigger::Click(By::Id("submitbutton"))),
+            Event::Assert(Assert::IsPresent("Your transaction")),// Transaction succeeds sometimes and pending sometimes
+    ]).await?;
+    Ok(())
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_3ds_payment_test() {
+    tester!(should_make_nuvei_3ds_payment, "firefox");
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_3ds_mandate_payment_test() {
+    tester!(should_make_nuvei_3ds_mandate_payment, "firefox");
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_gpay_payment_test() {
+    tester!(should_make_nuvei_gpay_payment, "firefox");
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_pypl_payment_test() {
+    tester!(should_make_nuvei_pypl_payment, "firefox");
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_giropay_payment_test() {
+    tester!(should_make_nuvei_giropay_payment, "firefox");
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_ideal_payment_test() {
+    tester!(should_make_nuvei_ideal_payment, "firefox");
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_sofort_payment_test() {
+    tester!(should_make_nuvei_sofort_payment, "firefox");
+}
+
+#[test]
+#[serial]
+fn should_make_nuvei_eps_payment_test() {
+    tester!(should_make_nuvei_eps_payment, "firefox");
+}
diff --git a/crates/router/tests/connectors/selenium.rs b/crates/router/tests/connectors/selenium.rs
new file mode 100644
index 00000000000..0114d510b30
--- /dev/null
+++ b/crates/router/tests/connectors/selenium.rs
@@ -0,0 +1,469 @@
+use std::{collections::HashMap, env, path::MAIN_SEPARATOR, time::Duration};
+
+use actix_web::cookie::SameSite;
+use async_trait::async_trait;
+use futures::Future;
+use thirtyfour::{components::SelectElement, prelude::*, WebDriver};
+
+pub enum Event<'a> {
+    RunIf(Assert<'a>, Vec<Event<'a>>),
+    EitherOr(Assert<'a>, Vec<Event<'a>>, Vec<Event<'a>>),
+    Assert(Assert<'a>),
+    Trigger(Trigger<'a>),
+}
+
+#[allow(dead_code)]
+pub enum Trigger<'a> {
+    Goto(&'a str),
+    Click(By),
+    ClickNth(By, usize),
+    SelectOption(By, &'a str),
+    ChangeQueryParam(&'a str, &'a str),
+    SwitchTab(Position),
+    SwitchFrame(By),
+    Find(By),
+    Query(By),
+    SendKeys(By, &'a str),
+    Sleep(u64),
+}
+
+pub enum Position {
+    Prev,
+    Next,
+}
+pub enum Selector {
+    Title,
+    QueryParamStr,
+}
+
+pub enum Assert<'a> {
+    Eq(Selector, &'a str),
+    Contains(Selector, &'a str),
+    IsPresent(&'a str),
+}
+
+#[async_trait]
+pub trait SeleniumTest {
+    async fn complete_actions(
+        &self,
+        driver: &WebDriver,
+        actions: Vec<Event<'_>>,
+    ) -> Result<(), WebDriverError> {
+        for action in actions {
+            match action {
+                Event::Assert(assert) => match assert {
+                    Assert::Contains(selector, text) => match selector {
+                        Selector::QueryParamStr => {
+                            let url = driver.current_url().await?;
+                            assert!(url.query().unwrap().contains(text))
+                        }
+                        _ => assert!(driver.title().await?.contains(text)),
+                    },
+                    Assert::Eq(_selector, text) => assert_eq!(driver.title().await?, text),
+                    Assert::IsPresent(text) => {
+                        assert!(is_text_present(driver, text).await?)
+                    }
+                },
+                Event::RunIf(con_event, events) => match con_event {
+                    Assert::Contains(selector, text) => match selector {
+                        Selector::QueryParamStr => {
+                            let url = driver.current_url().await?;
+                            if url.query().unwrap().contains(text) {
+                                self.complete_actions(driver, events).await?;
+                            }
+                        }
+                        _ => assert!(driver.title().await?.contains(text)),
+                    },
+                    Assert::Eq(_selector, text) => {
+                        if text == driver.title().await? {
+                            self.complete_actions(driver, events).await?;
+                        }
+                    }
+                    Assert::IsPresent(text) => {
+                        if is_text_present(driver, text).await.is_ok() {
+                            self.complete_actions(driver, events).await?;
+                        }
+                    }
+                },
+                Event::EitherOr(con_event, success, failure) => match con_event {
+                    Assert::Contains(selector, text) => match selector {
+                        Selector::QueryParamStr => {
+                            let url = driver.current_url().await?;
+                            self.complete_actions(
+                                driver,
+                                if url.query().unwrap().contains(text) {
+                                    success
+                                } else {
+                                    failure
+                                },
+                            )
+                            .await?;
+                        }
+                        _ => assert!(driver.title().await?.contains(text)),
+                    },
+                    Assert::Eq(_selector, text) => {
+                        self.complete_actions(
+                            driver,
+                            if text == driver.title().await? {
+                                success
+                            } else {
+                                failure
+                            },
+                        )
+                        .await?;
+                    }
+                    Assert::IsPresent(text) => {
+                        self.complete_actions(
+                            driver,
+                            if is_text_present(driver, text).await.is_ok() {
+                                success
+                            } else {
+                                failure
+                            },
+                        )
+                        .await?;
+                    }
+                },
+                Event::Trigger(trigger) => match trigger {
+                    Trigger::Goto(url) => {
+                        driver.goto(url).await?;
+                        let hs_base_url =
+                            env::var("HS_BASE_URL").unwrap_or("http://localhost:8080".to_string()); //Issue: #924
+                        let hs_api_key =
+                            env::var("HS_API_KEY").expect("Hyperswitch user API key not present"); //Issue: #924
+                        driver
+                            .add_cookie(new_cookie("hs_base_url", hs_base_url).clone())
+                            .await?;
+                        driver
+                            .add_cookie(new_cookie("hs_api_key", hs_api_key).clone())
+                            .await?;
+                    }
+                    Trigger::Click(by) => {
+                        let ele = driver.query(by).first().await?;
+                        ele.wait_until().displayed().await?;
+                        ele.wait_until().clickable().await?;
+                        ele.click().await?;
+                    }
+                    Trigger::ClickNth(by, n) => {
+                        let ele = driver.query(by).all().await?.into_iter().nth(n).unwrap();
+                        ele.wait_until().displayed().await?;
+                        ele.wait_until().clickable().await?;
+                        ele.click().await?;
+                    }
+                    Trigger::Find(by) => {
+                        driver.find(by).await?;
+                    }
+                    Trigger::Query(by) => {
+                        driver.query(by).first().await?;
+                    }
+                    Trigger::SendKeys(by, input) => {
+                        let ele = driver.query(by).first().await?;
+                        ele.wait_until().displayed().await?;
+                        ele.send_keys(&input).await?;
+                    }
+                    Trigger::SelectOption(by, input) => {
+                        let ele = driver.query(by).first().await?;
+                        let select_element = SelectElement::new(&ele).await?;
+                        select_element.select_by_partial_text(input).await?;
+                    }
+                    Trigger::ChangeQueryParam(param, value) => {
+                        let mut url = driver.current_url().await?;
+                        let mut hash_query: HashMap<String, String> =
+                            url.query_pairs().into_owned().collect();
+                        hash_query.insert(param.to_string(), value.to_string());
+                        let url_str = serde_urlencoded::to_string(hash_query)
+                            .expect("Query Param update failed");
+                        url.set_query(Some(&url_str));
+                        driver.goto(url.as_str()).await?;
+                    }
+                    Trigger::Sleep(seconds) => {
+                        tokio::time::sleep(Duration::from_secs(seconds)).await;
+                    }
+                    Trigger::SwitchTab(position) => match position {
+                        Position::Next => {
+                            let windows = driver.windows().await?;
+                            if let Some(window) = windows.iter().rev().next() {
+                                driver.switch_to_window(window.to_owned()).await?;
+                            }
+                        }
+                        Position::Prev => {
+                            let windows = driver.windows().await?;
+                            if let Some(window) = windows.into_iter().next() {
+                                driver.switch_to_window(window.to_owned()).await?;
+                            }
+                        }
+                    },
+                    Trigger::SwitchFrame(by) => {
+                        let iframe = driver.query(by).first().await?;
+                        iframe.wait_until().displayed().await?;
+                        iframe.clone().enter_frame().await?;
+                    }
+                },
+            }
+        }
+        Ok(())
+    }
+
+    async fn process_payment<F, Fut>(&self, _f: F) -> Result<(), WebDriverError>
+    where
+        F: FnOnce(WebDriver) -> Fut + Send,
+        Fut: Future<Output = Result<(), WebDriverError>> + Send,
+    {
+        let _browser = env::var("HS_TEST_BROWSER").unwrap_or("chrome".to_string()); //Issue: #924
+        Ok(())
+    }
+    async fn make_redirection_payment(
+        &self,
+        c: WebDriver,
+        actions: Vec<Event<'_>>,
+    ) -> Result<(), WebDriverError> {
+        self.complete_actions(&c, actions).await
+    }
+    async fn make_gpay_payment(
+        &self,
+        c: WebDriver,
+        url: &str,
+        actions: Vec<Event<'_>>,
+    ) -> Result<(), WebDriverError> {
+        let (email, pass) = (
+            &get_env("GMAIL_EMAIL").clone(),
+            &get_env("GMAIL_PASS").clone(),
+        );
+        let default_actions = vec![
+            Event::Trigger(Trigger::Goto(url)),
+            Event::Trigger(Trigger::Click(By::Css("#gpay-btn button"))),
+            Event::Trigger(Trigger::SwitchTab(Position::Next)),
+            Event::RunIf(
+                Assert::IsPresent("Sign in"),
+                vec![
+                    Event::Trigger(Trigger::SendKeys(By::Id("identifierId"), email)),
+                    Event::Trigger(Trigger::ClickNth(By::Tag("button"), 2)),
+                    Event::EitherOr(
+                        Assert::IsPresent("Welcome"),
+                        vec![
+                            Event::Trigger(Trigger::SendKeys(By::Name("Passwd"), pass)),
+                            Event::Trigger(Trigger::Sleep(2)),
+                            Event::Trigger(Trigger::Click(By::Id("passwordNext"))),
+                        ],
+                        vec![
+                            Event::Trigger(Trigger::SendKeys(By::Id("identifierId"), email)),
+                            Event::Trigger(Trigger::ClickNth(By::Tag("button"), 2)),
+                            Event::Trigger(Trigger::SendKeys(By::Name("Passwd"), pass)),
+                            Event::Trigger(Trigger::Sleep(2)),
+                            Event::Trigger(Trigger::Click(By::Id("passwordNext"))),
+                        ],
+                    ),
+                ],
+            ),
+            Event::Trigger(Trigger::Query(By::ClassName(
+                "bootstrapperIframeContainerElement",
+            ))),
+            Event::Trigger(Trigger::SwitchFrame(By::Id("sM432dIframe"))),
+            Event::Assert(Assert::IsPresent("Gpay Tester")),
+            Event::Trigger(Trigger::Click(By::ClassName("jfk-button-action"))),
+            Event::Trigger(Trigger::SwitchTab(Position::Prev)),
+        ];
+        self.complete_actions(&c, default_actions).await?;
+        self.complete_actions(&c, actions).await
+    }
+    async fn make_paypal_payment(
+        &self,
+        c: WebDriver,
+        url: &str,
+        actions: Vec<Event<'_>>,
+    ) -> Result<(), WebDriverError> {
+        self.complete_actions(
+            &c,
+            vec![
+                Event::Trigger(Trigger::Goto(url)),
+                Event::Trigger(Trigger::Click(By::Id("pypl-redirect-btn"))),
+            ],
+        )
+        .await?;
+        let (email, pass) = (
+            &get_env("PYPL_EMAIL").clone(),
+            &get_env("PYPL_PASS").clone(),
+        );
+        let mut pypl_actions = vec![
+            Event::EitherOr(
+                Assert::IsPresent("Password"),
+                vec![
+                    Event::Trigger(Trigger::SendKeys(By::Id("password"), pass)),
+                    Event::Trigger(Trigger::Click(By::Id("btnLogin"))),
+                ],
+                vec![
+                    Event::Trigger(Trigger::SendKeys(By::Id("email"), email)),
+                    Event::Trigger(Trigger::Click(By::Id("btnNext"))),
+                    Event::Trigger(Trigger::SendKeys(By::Id("password"), pass)),
+                    Event::Trigger(Trigger::Click(By::Id("btnLogin"))),
+                ],
+            ),
+            Event::Trigger(Trigger::Click(By::Id("payment-submit-btn"))),
+        ];
+        pypl_actions.extend(actions);
+        self.complete_actions(&c, pypl_actions).await
+    }
+}
+async fn is_text_present(driver: &WebDriver, key: &str) -> WebDriverResult<bool> {
+    let mut xpath = "//*[contains(text(),'".to_owned();
+    xpath.push_str(key);
+    xpath.push_str("')]");
+    let result = driver.query(By::XPath(&xpath)).first().await?;
+    result.is_present().await
+}
+fn new_cookie(name: &str, value: String) -> Cookie<'_> {
+    let mut base_url_cookie = Cookie::new(name, value);
+    base_url_cookie.set_same_site(Some(SameSite::Lax));
+    base_url_cookie.set_domain("hs-payment-tests.w3spaces.com");
+    base_url_cookie.set_path("/");
+    base_url_cookie
+}
+
+#[macro_export]
+macro_rules! tester_inner {
+    ($execute:ident, $webdriver:expr) => {{
+        use std::{
+            sync::{Arc, Mutex},
+            thread,
+        };
+
+        let driver = $webdriver;
+
+        // we'll need the session_id from the thread
+        // NOTE: even if it panics, so can't just return it
+        let session_id = Arc::new(Mutex::new(None));
+
+        // run test in its own thread to catch panics
+        let sid = session_id.clone();
+        let res = thread::spawn(move || {
+            let runtime = tokio::runtime::Builder::new_current_thread()
+                .enable_all()
+                .build()
+                .unwrap();
+            let driver = runtime
+                .block_on(driver)
+                .expect("failed to construct test WebDriver");
+            *sid.lock().unwrap() = runtime.block_on(driver.session_id()).ok();
+            // make sure we close, even if an assertion fails
+            let client = driver.clone();
+            let x = runtime.block_on(async move {
+                let r = tokio::spawn($execute(driver)).await;
+                let _ = client.quit().await;
+                r
+            });
+            drop(runtime);
+            x.expect("test panicked")
+        })
+        .join();
+        let success = handle_test_error(res);
+        assert!(success);
+    }};
+}
+
+#[macro_export]
+macro_rules! tester {
+    ($f:ident, $endpoint:expr) => {{
+        use $crate::tester_inner;
+
+        let url = make_url($endpoint);
+        let caps = make_capabilities($endpoint);
+        tester_inner!($f, WebDriver::new(url, caps));
+    }};
+}
+
+pub fn make_capabilities(s: &str) -> Capabilities {
+    match s {
+        "firefox" => {
+            let mut caps = DesiredCapabilities::firefox();
+            let profile_path = &format!("-profile={}", get_firefox_profile_path().unwrap());
+            caps.add_firefox_arg(profile_path).unwrap();
+            // let mut prefs = FirefoxPreferences::new();
+            // prefs.set("-browser.link.open_newwindow", 3).unwrap();
+            // caps.set_preferences(prefs).unwrap();
+            caps.into()
+        }
+        "chrome" => {
+            let mut caps = DesiredCapabilities::chrome();
+            let profile_path = &format!("user-data-dir={}", get_chrome_profile_path().unwrap());
+            caps.add_chrome_arg(profile_path).unwrap();
+            // caps.set_headless().unwrap();
+            // caps.set_no_sandbox().unwrap();
+            // caps.set_disable_gpu().unwrap();
+            // caps.set_disable_dev_shm_usage().unwrap();
+            caps.into()
+        }
+        &_ => DesiredCapabilities::safari().into(),
+    }
+}
+fn get_chrome_profile_path() -> Result<String, WebDriverError> {
+    env::var("CHROME_PROFILE_PATH").map_or_else(
+        //Issue: #924
+        |_| -> Result<String, WebDriverError> {
+            let exe = env::current_exe()?;
+            let dir = exe.parent().expect("Executable must be in some directory");
+            let mut base_path = dir
+                .to_str()
+                .map(|str| {
+                    let mut fp = str.split(MAIN_SEPARATOR).collect::<Vec<_>>();
+                    fp.truncate(3);
+                    fp.join(&MAIN_SEPARATOR.to_string())
+                })
+                .unwrap();
+            base_path.push_str(r#"/Library/Application\ Support/Google/Chrome/Default"#);
+            Ok(base_path)
+        },
+        Ok,
+    )
+}
+fn get_firefox_profile_path() -> Result<String, WebDriverError> {
+    env::var("FIREFOX_PROFILE_PATH").map_or_else(
+        //Issue: #924
+        |_| -> Result<String, WebDriverError> {
+            let exe = env::current_exe()?;
+            let dir = exe.parent().expect("Executable must be in some directory");
+            let mut base_path = dir
+                .to_str()
+                .map(|str| {
+                    let mut fp = str.split(MAIN_SEPARATOR).collect::<Vec<_>>();
+                    fp.truncate(3);
+                    fp.join(&MAIN_SEPARATOR.to_string())
+                })
+                .unwrap();
+            base_path.push_str(r#"/Library/Application Support/Firefox/Profiles/hs-test"#);
+            Ok(base_path)
+        },
+        Ok,
+    )
+}
+
+pub fn make_url(s: &str) -> &'static str {
+    match s {
+        "firefox" => "http://localhost:4444",
+        "chrome" => "http://localhost:9515",
+        &_ => "",
+    }
+}
+
+pub fn handle_test_error(
+    res: Result<Result<(), WebDriverError>, Box<dyn std::any::Any + Send>>,
+) -> bool {
+    match res {
+        Ok(Ok(_)) => true,
+        Ok(Err(e)) => {
+            eprintln!("test future failed to resolve: {:?}", e);
+            false
+        }
+        Err(e) => {
+            if let Some(e) = e.downcast_ref::<WebDriverError>() {
+                eprintln!("test future panicked: {:?}", e);
+            } else {
+                eprintln!("test future panicked; an assertion probably failed");
+            }
+            false
+        }
+    }
+}
+
+pub fn get_env(name: &str) -> String {
+    env::var(name).unwrap_or_else(|_| panic!("{name} not present")) //Issue: #924
+}
diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs
index f803252e6d0..6aa9742dee8 100644
--- a/crates/router/tests/connectors/utils.rs
+++ b/crates/router/tests/connectors/utils.rs
@@ -400,6 +400,7 @@ pub trait ConnectorActions: Connector {
             Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None,
             Ok(types::PaymentsResponseData::TokenizationResponse { .. }) => None,
             Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None,
+            Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None,
             Err(_) => None,
         }
     }
@@ -581,6 +582,7 @@ pub fn get_connector_transaction_id(
         Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None,
         Ok(types::PaymentsResponseData::TokenizationResponse { .. }) => None,
         Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None,
+        Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None,
         Err(_) => None,
     }
 }

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

- [ ] Bugfix
- [ ] New feature
- [X] Enhancement
- [ ] Refactoring
- [ ] Dependency updates

## Description
<!-- Describe your changes in detail -->
1. Add support for bank redirect Eps, Sofort, Giropay, Ideal
2. Add UI tests for redirection flow using selenium

### Additional Changes

- [ ] 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 its an obvious bug or documentation fix
that will have little conversation).
-->
Add support for card mandates and add tests

## 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)?
-->
<img width="1132" alt="Screen Shot 2023-04-13 at 4 04 18 AM" src="https://user-images.githubusercontent.com/20727598/231612357-875adfe1-a9b6-4b6c-83a6-25ebeab7de5f.png">


## 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 submitted code
- [X] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
