Bug: [BUG] Nuvei recurring MIT payment is not handled in Authorize flow



### Bug Description

In the Nuvei Authorize flow implementation, the `get_request_body` method currently utilizes `TryFrom` to construct a `NuveiPaymentsRequest` using `RouterData`. To accommodate `NuveiPaymentsRequest` for each payment method, we have a match statement where `payments::PaymentMethodData::MandatePayment` needs to implement `TryFrom` for building `NuveiPaymentsRequest` specifically for recurring mandates. The logic already present in the `get_card_info` method should be relocated inside the new `TryFrom` implementation for `payments::PaymentMethodData::MandatePayment`

### Expected Behavior

Recurring MIT payments should work with connector_mandate_id for Nuvei connector.

### Actual Behavior

Recurring MIT payment with mandate id through Nuvei is failing saying "Selected payment method through nuvei is not implemented". 

### Steps To Reproduce

1. Create CIT payment with nuvei connector.
2. Use mandate_id from the previous CIT payment and create MIT payment
3. Hyperswitch throws 501 http status code in the response

### Have you spent some time checking if this bug has been raised before?

- [X] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)
diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs
index d5dd6e813ae..fe17fae8770 100644
--- a/crates/router/src/connector/nuvei/transformers.rs
+++ b/crates/router/src/connector/nuvei/transformers.rs
@@ -20,7 +20,7 @@ use crate::{
     consts,
     core::errors,
     services,
-    types::{self, api, storage::enums, transformers::ForeignTryFrom},
+    types::{self, api, storage::enums, transformers::ForeignTryFrom, BrowserInformation},
     utils::OptionExt,
 };
 
@@ -75,6 +75,7 @@ pub struct NuveiPaymentsRequest {
     pub transaction_type: TransactionType,
     pub is_rebilling: Option<String>,
     pub payment_option: PaymentOption,
+    pub device_details: Option<DeviceDetails>,
     pub checksum: String,
     pub billing_address: Option<BillingAddress>,
     pub related_transaction_id: Option<String>,
@@ -135,7 +136,6 @@ pub struct PaymentOption {
     pub card: Option<Card>,
     pub redirect_url: Option<Url>,
     pub user_payment_option_id: Option<String>,
-    pub device_details: Option<DeviceDetails>,
     pub alternative_payment_method: Option<AlternativePaymentMethod>,
     pub billing_address: Option<BillingAddress>,
 }
@@ -315,7 +315,7 @@ pub struct V2AdditionalParams {
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct DeviceDetails {
-    pub ip_address: String,
+    pub ip_address: Secret<String, pii::IpAddress>,
 }
 
 impl From<enums::CaptureMethod> for TransactionType {
@@ -746,6 +746,7 @@ impl<F>
         let item = data.0;
         let request_data = match item.request.payment_method_data.clone() {
             api::PaymentMethodData::Card(card) => get_card_info(item, &card),
+            api::PaymentMethodData::MandatePayment => Self::try_from(item),
             api::PaymentMethodData::Wallet(wallet) => match wallet {
                 payments::WalletData::GooglePay(gpay_data) => Self::try_from(gpay_data),
                 payments::WalletData::ApplePay(apple_pay_data) => Ok(Self::from(apple_pay_data)),
@@ -845,7 +846,6 @@ impl<F>
             payments::PaymentMethodData::BankDebit(_)
             | payments::PaymentMethodData::BankTransfer(_)
             | payments::PaymentMethodData::Crypto(_)
-            | payments::PaymentMethodData::MandatePayment
             | payments::PaymentMethodData::Reward
             | payments::PaymentMethodData::Upi(_)
             | payments::PaymentMethodData::Voucher(_)
@@ -874,6 +874,7 @@ impl<F>
             related_transaction_id: request_data.related_transaction_id,
             payment_option: request_data.payment_option,
             billing_address: request_data.billing_address,
+            device_details: request_data.device_details,
             url_details: Some(UrlDetails {
                 success_url: return_url.clone(),
                 failure_url: return_url.clone(),
@@ -888,104 +889,110 @@ fn get_card_info<F>(
     item: &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
     card_details: &payments::Card,
 ) -> Result<NuveiPaymentsRequest, error_stack::Report<errors::ConnectorError>> {
-    let browser_info = item.request.get_browser_info()?;
+    let browser_information = item.request.browser_info.clone();
     let related_transaction_id = if item.is_three_ds() {
         item.request.related_transaction_id.clone()
     } else {
         None
     };
-    let connector_mandate_id = &item.request.connector_mandate_id();
-    if connector_mandate_id.is_some() {
-        Ok(NuveiPaymentsRequest {
-            related_transaction_id,
-            is_rebilling: Some("1".to_string()), // In case of second installment, rebilling should be 1
-            user_token_id: Some(item.request.get_email()?),
-            payment_option: PaymentOption {
-                user_payment_option_id: connector_mandate_id.clone(),
-                ..Default::default()
-            },
-            ..Default::default()
-        })
-    } else {
-        let (is_rebilling, additional_params, user_token_id) =
-            match item.request.setup_mandate_details.clone() {
-                Some(mandate_data) => {
-                    let details = match mandate_data
-                        .mandate_type
-                        .get_required_value("mandate_type")
-                        .change_context(errors::ConnectorError::MissingRequiredField {
-                            field_name: "mandate_type",
-                        })? {
-                        MandateDataType::SingleUse(details) => details,
-                        MandateDataType::MultiUse(details) => {
-                            details.ok_or(errors::ConnectorError::MissingRequiredField {
-                                field_name: "mandate_data.mandate_type.multi_use",
-                            })?
-                        }
-                    };
-                    let mandate_meta: NuveiMandateMeta = utils::to_connector_meta_from_secret(
-                        Some(details.get_metadata().ok_or_else(utils::missing_field_err(
-                            "mandate_data.mandate_type.{multi_use|single_use}.metadata",
-                        ))?),
-                    )?;
-                    (
-                        Some("0".to_string()), // In case of first installment, rebilling should be 0
-                        Some(V2AdditionalParams {
-                            rebill_expiry: Some(
-                                details
-                                    .get_end_date(date_time::DateFormat::YYYYMMDD)
-                                    .change_context(errors::ConnectorError::DateFormattingFailed)?
-                                    .ok_or_else(utils::missing_field_err(
-                                        "mandate_data.mandate_type.{multi_use|single_use}.end_date",
-                                    ))?,
-                            ),
-                            rebill_frequency: Some(mandate_meta.frequency),
-                            challenge_window_size: None,
-                        }),
-                        Some(item.request.get_email()?),
-                    )
-                }
-                _ => (None, None, None),
-            };
-        let three_d = if item.is_three_ds() {
-            Some(ThreeD {
-                browser_details: Some(BrowserDetails {
-                    accept_header: browser_info.get_accept_header()?,
-                    ip: browser_info.get_ip_address()?,
-                    java_enabled: browser_info.get_java_enabled()?.to_string().to_uppercase(),
-                    java_script_enabled: browser_info
-                        .get_java_script_enabled()?
-                        .to_string()
-                        .to_uppercase(),
-                    language: browser_info.get_language()?,
-                    screen_height: browser_info.get_screen_height()?,
-                    screen_width: browser_info.get_screen_width()?,
-                    color_depth: browser_info.get_color_depth()?,
-                    user_agent: browser_info.get_user_agent()?,
-                    time_zone: browser_info.get_time_zone()?,
-                }),
-                v2_additional_params: additional_params,
-                notification_url: item.request.complete_authorize_url.clone(),
-                merchant_url: item.return_url.clone(),
-                platform_type: Some(PlatformType::Browser),
-                method_completion_ind: Some(MethodCompletion::Unavailable),
-                ..Default::default()
-            })
-        } else {
-            None
-        };
 
-        Ok(NuveiPaymentsRequest {
-            related_transaction_id,
-            is_rebilling,
-            user_token_id,
-            payment_option: PaymentOption::from(NuveiCardDetails {
-                card: card_details.clone(),
-                three_d,
+    let address = item.get_billing_address_details_as_optional();
+
+    let billing_address = match address {
+        Some(address) => Some(BillingAddress {
+            first_name: Some(address.get_first_name()?.clone()),
+            last_name: Some(address.get_last_name()?.clone()),
+            email: item.request.get_email()?,
+            country: item.get_billing_country()?,
+        }),
+        None => None,
+    };
+    let (is_rebilling, additional_params, user_token_id) =
+        match item.request.setup_mandate_details.clone() {
+            Some(mandate_data) => {
+                let details = match mandate_data
+                    .mandate_type
+                    .get_required_value("mandate_type")
+                    .change_context(errors::ConnectorError::MissingRequiredField {
+                        field_name: "mandate_type",
+                    })? {
+                    MandateDataType::SingleUse(details) => details,
+                    MandateDataType::MultiUse(details) => {
+                        details.ok_or(errors::ConnectorError::MissingRequiredField {
+                            field_name: "mandate_data.mandate_type.multi_use",
+                        })?
+                    }
+                };
+                let mandate_meta: NuveiMandateMeta = utils::to_connector_meta_from_secret(Some(
+                    details.get_metadata().ok_or_else(utils::missing_field_err(
+                        "mandate_data.mandate_type.{multi_use|single_use}.metadata",
+                    ))?,
+                ))?;
+                (
+                    Some("0".to_string()), // In case of first installment, rebilling should be 0
+                    Some(V2AdditionalParams {
+                        rebill_expiry: Some(
+                            details
+                                .get_end_date(date_time::DateFormat::YYYYMMDD)
+                                .change_context(errors::ConnectorError::DateFormattingFailed)?
+                                .ok_or_else(utils::missing_field_err(
+                                    "mandate_data.mandate_type.{multi_use|single_use}.end_date",
+                                ))?,
+                        ),
+                        rebill_frequency: Some(mandate_meta.frequency),
+                        challenge_window_size: None,
+                    }),
+                    Some(item.request.get_email()?),
+                )
+            }
+            _ => (None, None, None),
+        };
+    let three_d = if item.is_three_ds() {
+        let browser_details = match &browser_information {
+            Some(browser_info) => Some(BrowserDetails {
+                accept_header: browser_info.get_accept_header()?,
+                ip: browser_info.get_ip_address()?,
+                java_enabled: browser_info.get_java_enabled()?.to_string().to_uppercase(),
+                java_script_enabled: browser_info
+                    .get_java_script_enabled()?
+                    .to_string()
+                    .to_uppercase(),
+                language: browser_info.get_language()?,
+                screen_height: browser_info.get_screen_height()?,
+                screen_width: browser_info.get_screen_width()?,
+                color_depth: browser_info.get_color_depth()?,
+                user_agent: browser_info.get_user_agent()?,
+                time_zone: browser_info.get_time_zone()?,
             }),
+            None => None,
+        };
+        Some(ThreeD {
+            browser_details,
+            v2_additional_params: additional_params,
+            notification_url: item.request.complete_authorize_url.clone(),
+            merchant_url: item.return_url.clone(),
+            platform_type: Some(PlatformType::Browser),
+            method_completion_ind: Some(MethodCompletion::Unavailable),
             ..Default::default()
         })
-    }
+    } else {
+        None
+    };
+
+    Ok(NuveiPaymentsRequest {
+        related_transaction_id,
+        is_rebilling,
+        user_token_id,
+        device_details: Option::<DeviceDetails>::foreign_try_from(
+            &item.request.browser_info.clone(),
+        )?,
+        payment_option: PaymentOption::from(NuveiCardDetails {
+            card: card_details.clone(),
+            three_d,
+        }),
+        billing_address,
+        ..Default::default()
+    })
 }
 impl From<NuveiCardDetails> for PaymentOption {
     fn from(card_details: NuveiCardDetails) -> Self {
@@ -1532,6 +1539,51 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, NuveiPaymentsResponse>
     }
 }
 
+impl<F> TryFrom<&types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>>
+    for NuveiPaymentsRequest
+{
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn try_from(
+        data: &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
+    ) -> Result<Self, Self::Error> {
+        {
+            let item = data;
+            let connector_mandate_id = &item.request.connector_mandate_id();
+            let related_transaction_id = if item.is_three_ds() {
+                item.request.related_transaction_id.clone()
+            } else {
+                None
+            };
+            Ok(Self {
+                related_transaction_id,
+                device_details: Option::<DeviceDetails>::foreign_try_from(
+                    &item.request.browser_info.clone(),
+                )?,
+                is_rebilling: Some("1".to_string()), // In case of second installment, rebilling should be 1
+                user_token_id: Some(item.request.get_email()?),
+                payment_option: PaymentOption {
+                    user_payment_option_id: connector_mandate_id.clone(),
+                    ..Default::default()
+                },
+                ..Default::default()
+            })
+        }
+    }
+}
+
+impl ForeignTryFrom<&Option<BrowserInformation>> for Option<DeviceDetails> {
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn foreign_try_from(browser_info: &Option<BrowserInformation>) -> Result<Self, Self::Error> {
+        let device_details = match browser_info {
+            Some(browser_info) => Some(DeviceDetails {
+                ip_address: browser_info.get_ip_address()?,
+            }),
+            None => None,
+        };
+        Ok(device_details)
+    }
+}
+
 fn get_refund_response(
     response: NuveiPaymentsResponse,
     http_code: u16,
diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs
index 7951246790f..1e4552d6577 100644
--- a/crates/router/src/connector/utils.rs
+++ b/crates/router/src/connector/utils.rs
@@ -86,6 +86,7 @@ pub trait RouterData {
     fn get_payout_method_data(&self) -> Result<api::PayoutMethodData, Error>;
     #[cfg(feature = "payouts")]
     fn get_quote_id(&self) -> Result<String, Error>;
+    fn get_billing_address_details_as_optional(&self) -> Option<api::AddressDetails>;
 }
 
 pub trait PaymentResponseRouterData {
@@ -182,6 +183,14 @@ impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Re
             .ok_or_else(missing_field_err("billing.address"))
     }
 
+    fn get_billing_address_details_as_optional(&self) -> Option<api::AddressDetails> {
+        self.address
+            .billing
+            .as_ref()
+            .and_then(|a| a.address.as_ref())
+            .cloned()
+    }
+
     fn get_billing_address_with_phone_number(&self) -> Result<&api::Address, Error> {
         self.address
             .billing

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

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

## Description

- Moved mandate logic outside card_info to MandatePayment
- Included Device detail to send IP address to nuvei (mandatory)  for both card_info and mandate flow , Ip address is taken from browser info
- Billing address for rawcard payment flow
- Moved broswer info inside three_ds request construction, since its optional for non-3ds card_info flow

https://github.com/juspay/hyperswitch/issues/3537
https://github.com/juspay/hyperswitch/issues/3511

### 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).
-->
Open Issues :
https://github.com/juspay/hyperswitch/issues/3537
https://github.com/juspay/hyperswitch/issues/3511
Changes done to support Recurring mandate payments flow for Nuvei and included fix for mandatory detail population, like device_details, billing address and browser_info




## 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)?
-->
Added screenshots from testing
Recurring pay with mandate
<img width="1127" alt="Screenshot 2024-02-08 at 6 49 58 PM" src="https://github.com/juspay/hyperswitch/assets/97145230/a6c7997d-c98e-4eb1-b5d3-27a71c26f1c4">

Payment With and without IP address 

<img width="1400" alt="Screenshot 2024-02-08 at 6 49 08 PM" src="https://github.com/juspay/hyperswitch/assets/97145230/cab36f5e-2a5d-4a42-8ef2-25e737ae8c7e">
<img width="1417" alt="Screenshot 2024-02-08 at 6 48 30 PM" src="https://github.com/juspay/hyperswitch/assets/97145230/46953001-166e-40b7-ab97-8ed08573d2b9">


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [X] I formatted the code `cargo +nightly fmt --all`
- [X] I addressed lints thrown by `cargo clippy`
- [X] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
