Bug: Allow off-session payments using Payment Method ID (only with API Key auth)



When a mandate is created on connector's end, we store the connector mandate details in our payment methods table. Now in order to make recurring payment with this, we basically have to do a token based payment where set the `setup_future_usage = off_session` and we list the payment methods of a customer and make a payment with payment token. But we should also allow off-session payments using payment method id without having to list customer payment methods. For this, we accept `off_session = true` and payment method id in the request and make a recurring mandate payment.
diff --git a/crates/api_models/src/mandates.rs b/crates/api_models/src/mandates.rs
index bd5c5b5a1a0..4adda0d9af4 100644
--- a/crates/api_models/src/mandates.rs
+++ b/crates/api_models/src/mandates.rs
@@ -113,3 +113,10 @@ pub struct MandateListConstraints {
     #[serde(rename = "created_time.gte")]
     pub created_time_gte: Option<PrimitiveDateTime>,
 }
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
+pub enum RecurringDetails {
+    MandateId(String),
+    PaymentMethodId(String),
+}
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index ce2b1c3d68a..72592ec3cad 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -19,10 +19,8 @@ use url::Url;
 use utoipa::ToSchema;
 
 use crate::{
-    admin, disputes,
-    enums::{self as api_enums},
-    ephemeral_key::EphemeralKeyCreateResponse,
-    refunds,
+    admin, disputes, enums as api_enums, ephemeral_key::EphemeralKeyCreateResponse,
+    mandates::RecurringDetails, refunds,
 };
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -459,6 +457,9 @@ pub struct PaymentsRequest {
     /// Whether to perform external authentication (if applicable)
     #[schema(example = true)]
     pub request_external_three_ds_authentication: Option<bool>,
+
+    /// Details required for recurring payment
+    pub recurring_details: Option<RecurringDetails>,
 }
 
 impl PaymentsRequest {
@@ -3360,7 +3361,7 @@ pub struct PaymentsRedirectionResponse {
 }
 
 pub struct MandateValidationFields {
-    pub mandate_id: Option<String>,
+    pub recurring_details: Option<RecurringDetails>,
     pub confirm: Option<bool>,
     pub customer_id: Option<String>,
     pub mandate_data: Option<MandateData>,
@@ -3370,8 +3371,14 @@ pub struct MandateValidationFields {
 
 impl From<&PaymentsRequest> for MandateValidationFields {
     fn from(req: &PaymentsRequest) -> Self {
+        let recurring_details = req
+            .mandate_id
+            .clone()
+            .map(RecurringDetails::MandateId)
+            .or(req.recurring_details.clone());
+
         Self {
-            mandate_id: req.mandate_id.clone(),
+            recurring_details,
             confirm: req.confirm,
             customer_id: req
                 .customer
@@ -3389,7 +3396,7 @@ impl From<&PaymentsRequest> for MandateValidationFields {
 impl From<&VerifyRequest> for MandateValidationFields {
     fn from(req: &VerifyRequest) -> Self {
         Self {
-            mandate_id: None,
+            recurring_details: None,
             confirm: Some(true),
             customer_id: req.customer_id.clone(),
             mandate_data: req.mandate_data.clone(),
diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs
index c8e89f2ee70..4a61a20d08d 100644
--- a/crates/openapi/src/openapi.rs
+++ b/crates/openapi/src/openapi.rs
@@ -410,6 +410,7 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::mandates::MandateRevokedResponse,
         api_models::mandates::MandateResponse,
         api_models::mandates::MandateCardDetails,
+        api_models::mandates::RecurringDetails,
         api_models::ephemeral_key::EphemeralKeyCreateResponse,
         api_models::payments::CustomerDetails,
         api_models::payments::GiftCardData,
diff --git a/crates/router/src/core/mandate/helpers.rs b/crates/router/src/core/mandate/helpers.rs
index 150130ed9e5..704b7ae99f5 100644
--- a/crates/router/src/core/mandate/helpers.rs
+++ b/crates/router/src/core/mandate/helpers.rs
@@ -1,8 +1,14 @@
+use common_enums::enums;
 use common_utils::errors::CustomResult;
+use data_models::mandates::MandateData;
 use diesel_models::Mandate;
 use error_stack::ResultExt;
 
-use crate::{core::errors, routes::AppState, types::domain};
+use crate::{
+    core::{errors, payments},
+    routes::AppState,
+    types::domain,
+};
 
 pub async fn get_profile_id_for_mandate(
     state: &AppState,
@@ -33,3 +39,14 @@ pub async fn get_profile_id_for_mandate(
     }?;
     Ok(profile_id)
 }
+
+#[derive(Clone)]
+pub struct MandateGenericData {
+    pub token: Option<String>,
+    pub payment_method: Option<enums::PaymentMethod>,
+    pub payment_method_type: Option<enums::PaymentMethodType>,
+    pub mandate_data: Option<MandateData>,
+    pub recurring_mandate_payment_data: Option<payments::RecurringMandatePaymentData>,
+    pub mandate_connector: Option<payments::MandateConnectorDetails>,
+    pub payment_method_info: Option<diesel_models::PaymentMethod>,
+}
diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs
index 0a3a93923b7..e11a77496af 100644
--- a/crates/router/src/core/payments.rs
+++ b/crates/router/src/core/payments.rs
@@ -15,6 +15,7 @@ use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoI
 
 use api_models::{
     self, enums,
+    mandates::RecurringDetails,
     payments::{self as payments_api, HeaderPayload},
 };
 use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge};
@@ -2263,6 +2264,7 @@ where
     pub authorizations: Vec<diesel_models::authorization::Authorization>,
     pub authentication: Option<storage::Authentication>,
     pub frm_metadata: Option<serde_json::Value>,
+    pub recurring_details: Option<RecurringDetails>,
 }
 
 #[derive(Debug, Default, Clone)]
@@ -2907,7 +2909,7 @@ where
             .change_context(errors::ApiErrorResponse::InternalServerError)
             .attach_printable("Invalid connector name received")?;
 
-        return decide_connector_for_token_based_mit_flow(
+        return decide_multiplex_connector_for_normal_or_recurring_payment(
             payment_data,
             routing_data,
             connector_data,
@@ -2961,7 +2963,7 @@ where
             .change_context(errors::ApiErrorResponse::InternalServerError)
             .attach_printable("Invalid connector name received")?;
 
-        return decide_connector_for_token_based_mit_flow(
+        return decide_multiplex_connector_for_normal_or_recurring_payment(
             payment_data,
             routing_data,
             connector_data,
@@ -2980,24 +2982,26 @@ where
     .await
 }
 
-pub fn decide_connector_for_token_based_mit_flow<F: Clone>(
+pub fn decide_multiplex_connector_for_normal_or_recurring_payment<F: Clone>(
     payment_data: &mut PaymentData<F>,
     routing_data: &mut storage::RoutingData,
     connectors: Vec<api::ConnectorData>,
 ) -> RouterResult<ConnectorCallType> {
-    if let Some((storage_enums::FutureUsage::OffSession, _)) = payment_data
-        .payment_intent
-        .setup_future_usage
-        .zip(payment_data.token_data.as_ref())
-    {
-        logger::debug!("performing routing for token-based MIT flow");
+    match (
+        payment_data.payment_intent.setup_future_usage,
+        payment_data.token_data.as_ref(),
+        payment_data.recurring_details.as_ref(),
+    ) {
+        (Some(storage_enums::FutureUsage::OffSession), Some(_), None)
+        | (None, None, Some(RecurringDetails::PaymentMethodId(_))) => {
+            logger::debug!("performing routing for token-based MIT flow");
 
-        let payment_method_info = payment_data
-            .payment_method_info
-            .as_ref()
-            .get_required_value("payment_method_info")?;
+            let payment_method_info = payment_data
+                .payment_method_info
+                .as_ref()
+                .get_required_value("payment_method_info")?;
 
-        let connector_mandate_details = payment_method_info
+            let connector_mandate_details = payment_method_info
             .connector_mandate_details
             .clone()
             .map(|details| {
@@ -3010,67 +3014,69 @@ pub fn decide_connector_for_token_based_mit_flow<F: Clone>(
             .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
             .attach_printable("no eligible connector found for token-based MIT flow since there were no connector mandate details")?;
 
-        let mut connector_choice = None;
-        for connector_data in connectors {
-            if let Some(merchant_connector_id) = connector_data.merchant_connector_id.as_ref() {
-                if let Some(mandate_reference_record) =
-                    connector_mandate_details.get(merchant_connector_id)
-                {
-                    connector_choice = Some((connector_data, mandate_reference_record.clone()));
-                    break;
+            let mut connector_choice = None;
+            for connector_data in connectors {
+                if let Some(merchant_connector_id) = connector_data.merchant_connector_id.as_ref() {
+                    if let Some(mandate_reference_record) =
+                        connector_mandate_details.get(merchant_connector_id)
+                    {
+                        connector_choice = Some((connector_data, mandate_reference_record.clone()));
+                        break;
+                    }
                 }
             }
-        }
 
-        let (chosen_connector_data, mandate_reference_record) = connector_choice
-            .get_required_value("connector_choice")
-            .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
-            .attach_printable("no eligible connector found for token-based MIT payment")?;
+            let (chosen_connector_data, mandate_reference_record) = connector_choice
+                .get_required_value("connector_choice")
+                .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
+                .attach_printable("no eligible connector found for token-based MIT payment")?;
 
-        routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string());
-        #[cfg(feature = "connector_choice_mca_id")]
-        {
-            routing_data.merchant_connector_id =
-                chosen_connector_data.merchant_connector_id.clone();
-        }
+            routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string());
+            #[cfg(feature = "connector_choice_mca_id")]
+            {
+                routing_data.merchant_connector_id =
+                    chosen_connector_data.merchant_connector_id.clone();
+            }
 
-        payment_data.mandate_id = Some(payments_api::MandateIds {
-            mandate_id: None,
-            mandate_reference_id: Some(payments_api::MandateReferenceId::ConnectorMandateId(
-                payments_api::ConnectorMandateReferenceId {
-                    connector_mandate_id: Some(
-                        mandate_reference_record.connector_mandate_id.clone(),
-                    ),
-                    payment_method_id: Some(payment_method_info.payment_method_id.clone()),
-                    update_history: None,
-                },
-            )),
-        });
-
-        payment_data.recurring_mandate_payment_data = Some(RecurringMandatePaymentData {
-            payment_method_type: mandate_reference_record.payment_method_type,
-            original_payment_authorized_amount: mandate_reference_record
-                .original_payment_authorized_amount,
-            original_payment_authorized_currency: mandate_reference_record
-                .original_payment_authorized_currency,
-        });
-
-        Ok(api::ConnectorCallType::PreDetermined(chosen_connector_data))
-    } else {
-        let first_choice = connectors
-            .first()
-            .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
-            .into_report()
-            .attach_printable("no eligible connector found for payment")?
-            .clone();
-
-        routing_data.routed_through = Some(first_choice.connector_name.to_string());
-        #[cfg(feature = "connector_choice_mca_id")]
-        {
-            routing_data.merchant_connector_id = first_choice.merchant_connector_id;
+            payment_data.mandate_id = Some(payments_api::MandateIds {
+                mandate_id: None,
+                mandate_reference_id: Some(payments_api::MandateReferenceId::ConnectorMandateId(
+                    payments_api::ConnectorMandateReferenceId {
+                        connector_mandate_id: Some(
+                            mandate_reference_record.connector_mandate_id.clone(),
+                        ),
+                        payment_method_id: Some(payment_method_info.payment_method_id.clone()),
+                        update_history: None,
+                    },
+                )),
+            });
+
+            payment_data.recurring_mandate_payment_data = Some(RecurringMandatePaymentData {
+                payment_method_type: mandate_reference_record.payment_method_type,
+                original_payment_authorized_amount: mandate_reference_record
+                    .original_payment_authorized_amount,
+                original_payment_authorized_currency: mandate_reference_record
+                    .original_payment_authorized_currency,
+            });
+
+            Ok(api::ConnectorCallType::PreDetermined(chosen_connector_data))
         }
+        _ => {
+            let first_choice = connectors
+                .first()
+                .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
+                .into_report()
+                .attach_printable("no eligible connector found for payment")?
+                .clone();
+
+            routing_data.routed_through = Some(first_choice.connector_name.to_string());
+            #[cfg(feature = "connector_choice_mca_id")]
+            {
+                routing_data.merchant_connector_id = first_choice.merchant_connector_id;
+            }
 
-        Ok(api::ConnectorCallType::Retryable(connectors))
+            Ok(api::ConnectorCallType::Retryable(connectors))
+        }
     }
 }
 
@@ -3291,7 +3297,11 @@ where
 
     match transaction_data {
         TransactionData::Payment(payment_data) => {
-            decide_connector_for_token_based_mit_flow(payment_data, routing_data, connector_data)
+            decide_multiplex_connector_for_normal_or_recurring_payment(
+                payment_data,
+                routing_data,
+                connector_data,
+            )
         }
 
         #[cfg(feature = "payouts")]
diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs
index 7eab4c0d26a..e9cefbf8aed 100644
--- a/crates/router/src/core/payments/helpers.rs
+++ b/crates/router/src/core/payments/helpers.rs
@@ -1,6 +1,9 @@
 use std::borrow::Cow;
 
-use api_models::payments::{CardToken, GetPaymentMethodType, RequestSurchargeDetails};
+use api_models::{
+    mandates::RecurringDetails,
+    payments::{CardToken, GetPaymentMethodType, RequestSurchargeDetails},
+};
 use base64::Engine;
 use common_utils::{
     ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt},
@@ -34,6 +37,7 @@ use crate::{
     consts::{self, BASE64_ENGINE},
     core::{
         errors::{self, CustomResult, RouterResult, StorageErrorExt},
+        mandate::helpers::MandateGenericData,
         payment_methods::{cards, vault, PaymentMethodRetrieve},
         payments,
         pm_auth::retrieve_payment_method_from_auth_service,
@@ -414,59 +418,117 @@ pub async fn get_token_pm_type_mandate_details(
     mandate_type: Option<api::MandateTransactionType>,
     merchant_account: &domain::MerchantAccount,
     merchant_key_store: &domain::MerchantKeyStore,
-) -> RouterResult<(
-    Option<String>,
-    Option<storage_enums::PaymentMethod>,
-    Option<storage_enums::PaymentMethodType>,
-    Option<MandateData>,
-    Option<payments::RecurringMandatePaymentData>,
-    Option<payments::MandateConnectorDetails>,
-)> {
+) -> RouterResult<MandateGenericData> {
     let mandate_data = request.mandate_data.clone().map(MandateData::foreign_from);
-    match mandate_type {
-        Some(api::MandateTransactionType::NewMandateTransaction) => {
-            let setup_mandate = mandate_data.clone().get_required_value("mandate_data")?;
-            Ok((
-                request.payment_token.to_owned(),
-                request.payment_method,
-                request.payment_method_type,
-                Some(setup_mandate),
-                None,
-                None,
-            ))
-        }
+    let (
+        payment_token,
+        payment_method,
+        payment_method_type,
+        mandate_data,
+        recurring_payment_data,
+        mandate_connector_details,
+        payment_method_info,
+    ) = match mandate_type {
+        Some(api::MandateTransactionType::NewMandateTransaction) => (
+            request.payment_token.to_owned(),
+            request.payment_method,
+            request.payment_method_type,
+            Some(mandate_data.clone().get_required_value("mandate_data")?),
+            None,
+            None,
+            None,
+        ),
         Some(api::MandateTransactionType::RecurringMandateTransaction) => {
-            let (
-                token_,
-                payment_method_,
-                recurring_mandate_payment_data,
-                payment_method_type_,
-                mandate_connector,
-            ) = get_token_for_recurring_mandate(
-                state,
-                request,
-                merchant_account,
-                merchant_key_store,
-            )
-            .await?;
-            Ok((
-                token_,
-                payment_method_,
-                payment_method_type_.or(request.payment_method_type),
-                None,
-                recurring_mandate_payment_data,
-                mandate_connector,
-            ))
+            match &request.recurring_details {
+                Some(recurring_details) => match recurring_details {
+                    RecurringDetails::MandateId(mandate_id) => {
+                        let mandate_generic_data = get_token_for_recurring_mandate(
+                            state,
+                            request,
+                            merchant_account,
+                            merchant_key_store,
+                            mandate_id.to_owned(),
+                        )
+                        .await?;
+
+                        (
+                            mandate_generic_data.token,
+                            mandate_generic_data.payment_method,
+                            mandate_generic_data
+                                .payment_method_type
+                                .or(request.payment_method_type),
+                            None,
+                            mandate_generic_data.recurring_mandate_payment_data,
+                            mandate_generic_data.mandate_connector,
+                            None,
+                        )
+                    }
+                    RecurringDetails::PaymentMethodId(payment_method_id) => {
+                        let payment_method_info = state
+                            .store
+                            .find_payment_method(payment_method_id)
+                            .await
+                            .to_not_found_response(
+                                errors::ApiErrorResponse::PaymentMethodNotFound,
+                            )?;
+
+                        (
+                            None,
+                            Some(payment_method_info.payment_method),
+                            payment_method_info.payment_method_type,
+                            None,
+                            None,
+                            None,
+                            Some(payment_method_info),
+                        )
+                    }
+                },
+                None => {
+                    let mandate_id = request
+                        .mandate_id
+                        .clone()
+                        .get_required_value("mandate_id")?;
+                    let mandate_generic_data = get_token_for_recurring_mandate(
+                        state,
+                        request,
+                        merchant_account,
+                        merchant_key_store,
+                        mandate_id,
+                    )
+                    .await?;
+                    (
+                        mandate_generic_data.token,
+                        mandate_generic_data.payment_method,
+                        mandate_generic_data
+                            .payment_method_type
+                            .or(request.payment_method_type),
+                        None,
+                        mandate_generic_data.recurring_mandate_payment_data,
+                        mandate_generic_data.mandate_connector,
+                        None,
+                    )
+                }
+            }
         }
-        None => Ok((
+        None => (
             request.payment_token.to_owned(),
             request.payment_method,
             request.payment_method_type,
             mandate_data,
             None,
             None,
-        )),
-    }
+            None,
+        ),
+    };
+    Ok(MandateGenericData {
+        token: payment_token,
+        payment_method,
+        payment_method_type,
+        mandate_data,
+        recurring_mandate_payment_data: recurring_payment_data,
+        mandate_connector: mandate_connector_details,
+        payment_method_info,
+    })
 }
 
 pub async fn get_token_for_recurring_mandate(
@@ -474,15 +536,9 @@ pub async fn get_token_for_recurring_mandate(
     req: &api::PaymentsRequest,
     merchant_account: &domain::MerchantAccount,
     merchant_key_store: &domain::MerchantKeyStore,
-) -> RouterResult<(
-    Option<String>,
-    Option<storage_enums::PaymentMethod>,
-    Option<payments::RecurringMandatePaymentData>,
-    Option<storage_enums::PaymentMethodType>,
-    Option<payments::MandateConnectorDetails>,
-)> {
+    mandate_id: String,
+) -> RouterResult<MandateGenericData> {
     let db = &*state.store;
-    let mandate_id = req.mandate_id.clone().get_required_value("mandate_id")?;
 
     let mandate = db
         .find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, mandate_id.as_str())
@@ -566,29 +622,33 @@ pub async fn get_token_for_recurring_mandate(
             }
         };
 
-        Ok((
-            Some(token),
-            Some(payment_method.payment_method),
-            Some(payments::RecurringMandatePaymentData {
+        Ok(MandateGenericData {
+            token: Some(token),
+            payment_method: Some(payment_method.payment_method),
+            recurring_mandate_payment_data: Some(payments::RecurringMandatePaymentData {
                 payment_method_type,
                 original_payment_authorized_amount,
                 original_payment_authorized_currency,
             }),
-            payment_method.payment_method_type,
-            Some(mandate_connector_details),
-        ))
+            payment_method_type: payment_method.payment_method_type,
+            mandate_connector: Some(mandate_connector_details),
+            mandate_data: None,
+            payment_method_info: None,
+        })
     } else {
-        Ok((
-            None,
-            Some(payment_method.payment_method),
-            Some(payments::RecurringMandatePaymentData {
+        Ok(MandateGenericData {
+            token: None,
+            payment_method: Some(payment_method.payment_method),
+            recurring_mandate_payment_data: Some(payments::RecurringMandatePaymentData {
                 payment_method_type,
                 original_payment_authorized_amount,
                 original_payment_authorized_currency,
             }),
-            payment_method.payment_method_type,
-            Some(mandate_connector_details),
-        ))
+            payment_method_type: payment_method.payment_method_type,
+            mandate_connector: Some(mandate_connector_details),
+            mandate_data: None,
+            payment_method_info: None,
+        })
     }
 }
 
@@ -792,7 +852,7 @@ pub fn validate_mandate(
     let req: api::MandateValidationFields = req.into();
     match req.validate_and_get_mandate_type().change_context(
         errors::ApiErrorResponse::MandateValidationFailed {
-            reason: "Expected one out of mandate_id and mandate_data but got both".into(),
+            reason: "Expected one out of recurring_details and mandate_data but got both".into(),
         },
     )? {
         Some(api::MandateTransactionType::NewMandateTransaction) => {
@@ -809,6 +869,23 @@ pub fn validate_mandate(
     }
 }
 
+pub fn validate_recurring_details_and_token(
+    recurring_details: &Option<RecurringDetails>,
+    payment_token: &Option<String>,
+) -> CustomResult<(), errors::ApiErrorResponse> {
+    utils::when(
+        recurring_details.is_some() && payment_token.is_some(),
+        || {
+            Err(report!(errors::ApiErrorResponse::PreconditionFailed {
+                message: "Expected one out of recurring_details and payment_token but got both"
+                    .into()
+            }))
+        },
+    )?;
+
+    Ok(())
+}
+
 fn validate_new_mandate_request(
     req: api::MandateValidationFields,
     is_confirm_operation: bool,
@@ -940,7 +1017,8 @@ pub fn create_complete_authorize_url(
 }
 
 fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult<()> {
-    req.mandate_id.check_value_present("mandate_id")?;
+    req.recurring_details
+        .check_value_present("recurring_details")?;
 
     req.customer_id.check_value_present("customer_id")?;
 
@@ -1950,7 +2028,8 @@ pub(crate) fn validate_payment_method_fields_present(
     utils::when(
         req.payment_method.is_some()
             && req.payment_method_data.is_none()
-            && req.payment_token.is_none(),
+            && req.payment_token.is_none()
+            && req.recurring_details.is_none(),
         || {
             Err(errors::ApiErrorResponse::MissingRequiredField {
                 field_name: "payment_method_data",
diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs
index 65b856364cc..a2132bed8c1 100644
--- a/crates/router/src/core/payments/operations/payment_approve.rs
+++ b/crates/router/src/core/payments/operations/payment_approve.rs
@@ -178,6 +178,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             frm_metadata: None,
             authentication: None,
+            recurring_details: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs
index b25c0e2b909..ab5feb5d9b5 100644
--- a/crates/router/src/core/payments/operations/payment_cancel.rs
+++ b/crates/router/src/core/payments/operations/payment_cancel.rs
@@ -187,6 +187,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             frm_metadata: None,
             authentication: None,
+            recurring_details: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs
index 47d339f15be..4445589f42f 100644
--- a/crates/router/src/core/payments/operations/payment_capture.rs
+++ b/crates/router/src/core/payments/operations/payment_capture.rs
@@ -231,6 +231,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             frm_metadata: None,
             authentication: None,
+            recurring_details: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs
index 8a773904ea3..b788ebc95f7 100644
--- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs
+++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs
@@ -10,6 +10,7 @@ use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, Valida
 use crate::{
     core::{
         errors::{self, CustomResult, RouterResult, StorageErrorExt},
+        mandate::helpers::MandateGenericData,
         payment_methods::PaymentMethodRetrieve,
         payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData},
         utils as core_utils,
@@ -71,17 +72,18 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             "confirm",
         )?;
 
-        let (
+        let MandateGenericData {
             token,
             payment_method,
             payment_method_type,
-            setup_mandate,
+            mandate_data,
             recurring_mandate_payment_data,
             mandate_connector,
-        ) = helpers::get_token_pm_type_mandate_details(
+            payment_method_info,
+        } = helpers::get_token_pm_type_mandate_details(
             state,
             request,
-            mandate_type.clone(),
+            mandate_type.to_owned(),
             merchant_account,
             key_store,
         )
@@ -225,7 +227,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
         payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata);
 
         // The operation merges mandate data from both request and payment_attempt
-        let setup_mandate = setup_mandate.map(Into::into);
+        let setup_mandate = mandate_data.map(Into::into);
 
         let mandate_details_present =
             payment_attempt.mandate_details.is_some() || request.mandate_data.is_some();
@@ -270,7 +272,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
                 .payment_method_data
                 .as_ref()
                 .map(|pmd| pmd.payment_method_data.clone()),
-            payment_method_info: None,
+            payment_method_info,
             force_sync: None,
             refunds: vec![],
             disputes: vec![],
@@ -291,6 +293,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             authentication: None,
             frm_metadata: None,
+            recurring_details: request.recurring_details.clone(),
         };
 
         let customer_details = Some(CustomerDetails {
@@ -455,6 +458,11 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve> ValidateRequest<F, api::Paymen
         let mandate_type =
             helpers::validate_mandate(request, payments::is_operation_confirm(self))?;
 
+        helpers::validate_recurring_details_and_token(
+            &request.recurring_details,
+            &request.payment_token,
+        )?;
+
         Ok((
             Box::new(self),
             operations::ValidateResult {
diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs
index cb4226bcfbe..a0caf5e1c94 100644
--- a/crates/router/src/core/payments/operations/payment_confirm.rs
+++ b/crates/router/src/core/payments/operations/payment_confirm.rs
@@ -15,6 +15,7 @@ use crate::{
         authentication,
         blocklist::utils as blocklist_utils,
         errors::{self, CustomResult, RouterResult, StorageErrorExt},
+        mandate::helpers::MandateGenericData,
         payment_methods::PaymentMethodRetrieve,
         payments::{
             self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress,
@@ -351,14 +352,15 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             .setup_future_usage
             .or(payment_intent.setup_future_usage);
 
-        let (
+        let MandateGenericData {
             token,
             payment_method,
             payment_method_type,
-            mut setup_mandate,
+            mandate_data,
             recurring_mandate_payment_data,
             mandate_connector,
-        ) = mandate_details;
+            payment_method_info,
+        } = mandate_details;
 
         let browser_info = request
             .browser_info
@@ -406,7 +408,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
 
             (Some(token_data), payment_method_info)
         } else {
-            (None, None)
+            (None, payment_method_info)
         };
 
         payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method);
@@ -478,7 +480,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             .or(payment_attempt.business_sub_label);
 
         // The operation merges mandate data from both request and payment_attempt
-        setup_mandate = setup_mandate.map(|mut sm| {
+        let setup_mandate = mandate_data.map(|mut sm| {
             sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type);
             sm.update_mandate_id = payment_attempt
                 .mandate_data
@@ -619,6 +621,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             frm_metadata: request.frm_metadata.clone(),
             authentication,
+            recurring_details: request.recurring_details.clone(),
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
@@ -1199,6 +1202,11 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve> ValidateRequest<F, api::Paymen
         let mandate_type =
             helpers::validate_mandate(request, payments::is_operation_confirm(self))?;
 
+        helpers::validate_recurring_details_and_token(
+            &request.recurring_details,
+            &request.payment_token,
+        )?;
+
         let payment_id = request
             .payment_id
             .clone()
diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs
index 58675bf62cd..6dce6db9d9b 100644
--- a/crates/router/src/core/payments/operations/payment_create.rs
+++ b/crates/router/src/core/payments/operations/payment_create.rs
@@ -1,6 +1,6 @@
 use std::marker::PhantomData;
 
-use api_models::enums::FrmSuggestion;
+use api_models::{enums::FrmSuggestion, mandates::RecurringDetails};
 use async_trait::async_trait;
 use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
 use data_models::{
@@ -19,6 +19,7 @@ use crate::{
     consts,
     core::{
         errors::{self, CustomResult, RouterResult, StorageErrorExt},
+        mandate::helpers::MandateGenericData,
         payment_link,
         payment_methods::PaymentMethodRetrieve,
         payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData},
@@ -104,14 +105,15 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
                 })?
         };
 
-        let (
+        let MandateGenericData {
             token,
             payment_method,
             payment_method_type,
-            setup_mandate,
+            mandate_data,
             recurring_mandate_payment_data,
             mandate_connector,
-        ) = helpers::get_token_pm_type_mandate_details(
+            payment_method_info,
+        } = helpers::get_token_pm_type_mandate_details(
             state,
             request,
             mandate_type,
@@ -291,6 +293,14 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
         let mandate_id = request
             .mandate_id
             .as_ref()
+            .or_else(|| {
+            request.recurring_details
+                .as_ref()
+                .and_then(|recurring_details| match recurring_details {
+                    RecurringDetails::MandateId(id) => Some(id),
+                    _ => None,
+                })
+        })
             .async_and_then(|mandate_id| async {
                 let mandate = db
                     .find_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id)
@@ -359,7 +369,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             .transpose()?;
 
         // The operation merges mandate data from both request and payment_attempt
-        let setup_mandate = setup_mandate.map(MandateData::from);
+        let setup_mandate = mandate_data.map(MandateData::from);
 
         let customer_acceptance = request.customer_acceptance.clone().map(From::from);
 
@@ -398,7 +408,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             token_data: None,
             confirm: request.confirm,
             payment_method_data: payment_method_data_after_card_bin_call,
-            payment_method_info: None,
+            payment_method_info,
             refunds: vec![],
             disputes: vec![],
             attempts: None,
@@ -419,6 +429,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             authentication: None,
             frm_metadata: request.frm_metadata.clone(),
+            recurring_details: request.recurring_details.clone(),
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
@@ -676,6 +687,11 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve> ValidateRequest<F, api::Paymen
         let mandate_type =
             helpers::validate_mandate(request, payments::is_operation_confirm(self))?;
 
+        helpers::validate_recurring_details_and_token(
+            &request.recurring_details,
+            &request.payment_token,
+        )?;
+
         if request.confirm.unwrap_or(false) {
             helpers::validate_pm_or_token_given(
                 &request.payment_method,
diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs
index 591ecb0a118..e1d58b032aa 100644
--- a/crates/router/src/core/payments/operations/payment_reject.rs
+++ b/crates/router/src/core/payments/operations/payment_reject.rs
@@ -174,6 +174,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             authentication: None,
             frm_metadata: None,
+            recurring_details: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs
index 805b5fb3630..25b419e3222 100644
--- a/crates/router/src/core/payments/operations/payment_session.rs
+++ b/crates/router/src/core/payments/operations/payment_session.rs
@@ -199,6 +199,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             authentication: None,
             frm_metadata: None,
+            recurring_details: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs
index 520336be0e1..465a2e5818d 100644
--- a/crates/router/src/core/payments/operations/payment_start.rs
+++ b/crates/router/src/core/payments/operations/payment_start.rs
@@ -186,6 +186,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             authentication: None,
             frm_metadata: None,
+            recurring_details: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs
index 101d997db21..4d616146908 100644
--- a/crates/router/src/core/payments/operations/payment_status.rs
+++ b/crates/router/src/core/payments/operations/payment_status.rs
@@ -462,6 +462,7 @@ async fn get_tracker_for_sync<
         authorizations,
         authentication,
         frm_metadata: None,
+        recurring_details: None,
     };
 
     let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs
index 2a0b99bc54b..684f7c4b698 100644
--- a/crates/router/src/core/payments/operations/payment_update.rs
+++ b/crates/router/src/core/payments/operations/payment_update.rs
@@ -1,6 +1,8 @@
 use std::marker::PhantomData;
 
-use api_models::{enums::FrmSuggestion, payments::RequestSurchargeDetails};
+use api_models::{
+    enums::FrmSuggestion, mandates::RecurringDetails, payments::RequestSurchargeDetails,
+};
 use async_trait::async_trait;
 use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
 use error_stack::{report, IntoReport, ResultExt};
@@ -11,6 +13,7 @@ use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, Valida
 use crate::{
     core::{
         errors::{self, CustomResult, RouterResult, StorageErrorExt},
+        mandate::helpers::MandateGenericData,
         payment_methods::PaymentMethodRetrieve,
         payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData},
         utils as core_utils,
@@ -94,17 +97,19 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
         )?;
 
         helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?;
-        let (
+
+        let MandateGenericData {
             token,
             payment_method,
             payment_method_type,
-            setup_mandate,
+            mandate_data,
             recurring_mandate_payment_data,
             mandate_connector,
-        ) = helpers::get_token_pm_type_mandate_details(
+            payment_method_info,
+        } = helpers::get_token_pm_type_mandate_details(
             state,
             request,
-            mandate_type.clone(),
+            mandate_type.to_owned(),
             merchant_account,
             key_store,
         )
@@ -263,6 +268,14 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
         let mandate_id = request
             .mandate_id
             .as_ref()
+            .or_else(|| {
+            request.recurring_details
+                .as_ref()
+                .and_then(|recurring_details| match recurring_details {
+                    RecurringDetails::MandateId(id) => Some(id),
+                    _ => None,
+                })
+        })
             .async_and_then(|mandate_id| async {
                 let mandate = db
                     .find_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id)
@@ -357,7 +370,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             .transpose()?;
 
         // The operation merges mandate data from both request and payment_attempt
-        let setup_mandate = setup_mandate.map(Into::into);
+        let setup_mandate = mandate_data.map(Into::into);
         let mandate_details_present =
             payment_attempt.mandate_details.is_some() || request.mandate_data.is_some();
         helpers::validate_mandate_data_and_future_usage(
@@ -405,7 +418,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
                 .payment_method_data
                 .as_ref()
                 .map(|pmd| pmd.payment_method_data.clone()),
-            payment_method_info: None,
+            payment_method_info,
             force_sync: None,
             refunds: vec![],
             disputes: vec![],
@@ -426,6 +439,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             authentication: None,
             frm_metadata: request.frm_metadata.clone(),
+            recurring_details: request.recurring_details.clone(),
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
@@ -737,6 +751,11 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve> ValidateRequest<F, api::Paymen
 
         let mandate_type = helpers::validate_mandate(request, false)?;
 
+        helpers::validate_recurring_details_and_token(
+            &request.recurring_details,
+            &request.payment_token,
+        )?;
+
         Ok((
             Box::new(self),
             operations::ValidateResult {
diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs
index ab7ab56d94f..f28b1d55c32 100644
--- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs
+++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs
@@ -152,6 +152,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
             authorizations: vec![],
             authentication: None,
             frm_metadata: None,
+            recurring_details: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse {
diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs
index 8e13f6c9935..d465300dd61 100644
--- a/crates/router/src/types/api/payments.rs
+++ b/crates/router/src/types/api/payments.rs
@@ -115,10 +115,11 @@ impl MandateValidationFieldsExt for MandateValidationFields {
     fn validate_and_get_mandate_type(
         &self,
     ) -> errors::CustomResult<Option<MandateTransactionType>, errors::ValidationError> {
-        match (&self.mandate_data, &self.mandate_id) {
+        match (&self.mandate_data, &self.recurring_details) {
             (None, None) => Ok(None),
             (Some(_), Some(_)) => Err(errors::ValidationError::InvalidValue {
-                message: "Expected one out of mandate_id and mandate_data but got both".to_string(),
+                message: "Expected one out of recurring_details and mandate_data but got both"
+                    .to_string(),
             })
             .into_report(),
             (_, Some(_)) => Ok(Some(MandateTransactionType::RecurringMandateTransaction)),
diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json
index 2005f9c11e0..fa9c7435632 100644
--- a/openapi/openapi_spec.json
+++ b/openapi/openapi_spec.json
@@ -13334,6 +13334,14 @@
             "description": "Whether to perform external authentication (if applicable)",
             "example": true,
             "nullable": true
+          },
+          "recurring_details": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/RecurringDetails"
+              }
+            ],
+            "nullable": true
           }
         }
       },
@@ -13726,6 +13734,14 @@
             "description": "Whether to perform external authentication (if applicable)",
             "example": true,
             "nullable": true
+          },
+          "recurring_details": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/RecurringDetails"
+              }
+            ],
+            "nullable": true
           }
         }
       },
@@ -14220,6 +14236,14 @@
             "description": "Whether to perform external authentication (if applicable)",
             "example": true,
             "nullable": true
+          },
+          "recurring_details": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/RecurringDetails"
+              }
+            ],
+            "nullable": true
           }
         }
       },
@@ -15220,6 +15244,14 @@
             "description": "Whether to perform external authentication (if applicable)",
             "example": true,
             "nullable": true
+          },
+          "recurring_details": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/RecurringDetails"
+              }
+            ],
+            "nullable": true
           }
         }
       },
@@ -16129,6 +16161,49 @@
           "disabled"
         ]
       },
+      "RecurringDetails": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "type",
+              "data"
+            ],
+            "properties": {
+              "type": {
+                "type": "string",
+                "enum": [
+                  "mandate_id"
+                ]
+              },
+              "data": {
+                "type": "string"
+              }
+            }
+          },
+          {
+            "type": "object",
+            "required": [
+              "type",
+              "data"
+            ],
+            "properties": {
+              "type": {
+                "type": "string",
+                "enum": [
+                  "payment_method_id"
+                ]
+              },
+              "data": {
+                "type": "string"
+              }
+            }
+          }
+        ],
+        "discriminator": {
+          "propertyName": "type"
+        }
+      },
       "RedirectResponse": {
         "type": "object",
         "properties": {

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

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

## Description
<!-- Describe your changes in detail -->
When a mandate is created on connector's end, we store the connector mandate details in our payment methods table. Now in order to make recurring payment with this, we basically have to do a token based payment where set the `setup_future_usage = off_session` and we list the payment methods of a customer and make a payment with payment token. But we should also allow off-session payments using payment method id without having to list customer payment methods. For this, we accept `off_session = true` and payment method id in the request and make a recurring mandate payment.

### Additional Changes

- [x] 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`
-->

Api contract changes - 
Added new field in `PaymentsRequest`: 

```
"recurring_details": {
      "type": "payment_method_id/mandate_id",
      "data": "String"
},
```

## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
1. mca creation - 

```
curl --location 'http://localhost:8080/account/merchant_1710841026/connectors' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data '{
    "connector_type": "fiz_operations",
    "business_country": "US",
    "business_label": "default",
    "connector_name": "cybersource",
    "connector_account_details": {
        "auth_type": "SignatureKey",
        "api_key": "abc",
        "api_secret": "xyz",
        "key1": "key"
    },
    "test_mode": true,
    "disabled": false,
    "payment_methods_enabled": [
        {
            "payment_method": "card",
            "payment_method_types": [
                {
                    "payment_method_type": "credit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        }
    ],
    "connector_webhook_details": {
        "merchant_secret": "Xavior"
    }
}'
```

2. Setup mandate on connector's end

```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: abc' \
--data-raw '{
    "amount": 0,
    "currency": "USD",
    "confirm": true,
    "profile_id": "pro_ByhEgJFlWSahAgSqaUZl",
    "capture_method": "automatic",
    "customer_id": "mandate_customer1",
    "email": "guest@example.com",
    "setup_future_usage": "off_session",
    "customer_acceptance": {
        "acceptance_type": "online"
    },
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4242424242424242",
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "Test Holder",
            "card_cvc": "737"
        }
    },
    "payment_type": "setup_mandate",
    "billing": {
        "address": {
            "city": "test",
            "country": "US",
            "line1": "here",
            "line2": "there",
            "line3": "anywhere",
            "zip": "560095",
            "state": "Washington",
            "first_name": "One",
            "last_name": "Two"
        },
        "phone": {
            "number": "1234567890",
            "country_code": "+1"
        },
        "email": "guest@example.com"
    },
    "authentication_type": "no_three_ds"
}'
```

db entry - 

![image](https://github.com/juspay/hyperswitch/assets/70657455/43f5bbf1-ad6e-4996-bcf0-561e18eaf2da)

3. Recurring payment using `payment_method_id` 

```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: abc' \
--data-raw '{
    "amount": 499,
    "currency": "USD",
    "confirm": true,
    "profile_id": "pro_ByhEgJFlWSahAgSqaUZl",
    "capture_method": "automatic",
    "customer_id": "mandate_customer1",
    "email": "guest@example.com",
    "off_session": true,
    "recurring_details": {
        "type": "payment_method_id",
        "data": "pm_lmTnIO5EdCiiMgRPrV9x"
    },
    "payment_method": "card",
    "billing": {
        "address": {
            "city": "test",
            "country": "US",
            "line1": "here",
            "line2": "there",
            "line3": "anywhere",
            "zip": "560095",
            "state": "Washington",
            "first_name": "One",
            "last_name": "Two"
        },
        "phone": {
            "number": "1234567890",
            "country_code": "+1"
        },
        "email": "guest@example.com"
    },
    "authentication_type": "no_three_ds"
}'
```

Payment succeded - 
![image](https://github.com/juspay/hyperswitch/assets/70657455/e315ac35-d96b-4b02-aa4e-69502724991b)


Also try creating mandate on hyperswitch end by providing mandate_data in setup mandate request and get the `mandate_id`. Do the recurring payment with `mandate_id`. Also try another recurring payment with below structure

```
    "recurring_details": {
        "type": "mandate_id",
        "data": "xyz"
    }
```

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