Bug: [FEATURE] add tokenization action handling to payment flow in v2



### Feature Description

This feature add connector-level tokenization to Hyperswitch V2, allowing secure storage of card data as tokens at payment connectors like Braintree. 

### Possible Implementation

For connector-level tokenization:
         1.When a payment comes with raw card data and the connector supports tokenization, send the card details to the connector’s tokenization API.
	2.	Receive the tokenized payment method ID.
	3.	Use the token for the subsequent authorization or capture call.

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

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

### Have you read the Contributing Guidelines?

- [x] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!
diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs
index 56db1b8d080..251d8dcc000 100644
--- a/crates/router/src/core/payments.rs
+++ b/crates/router/src/core/payments.rs
@@ -220,7 +220,7 @@ where
     let mut connector_http_status_code = None;
     let (payment_data, connector_response_data) = match connector {
         ConnectorCallType::PreDetermined(connector_data) => {
-            let (mca_type_details, updated_customer, router_data) =
+            let (mca_type_details, updated_customer, router_data, tokenization_action) =
                 call_connector_service_prerequisites(
                     state,
                     req_state.clone(),
@@ -260,6 +260,7 @@ where
                 mca_type_details,
                 router_data,
                 updated_customer,
+                tokenization_action,
             )
             .await?;
 
@@ -300,7 +301,7 @@ where
             let mut connectors = connectors.clone().into_iter();
             let connector_data = get_connector_data(&mut connectors)?;
 
-            let (mca_type_details, updated_customer, router_data) =
+            let (mca_type_details, updated_customer, router_data, tokenization_action) =
                 call_connector_service_prerequisites(
                     state,
                     req_state.clone(),
@@ -340,6 +341,7 @@ where
                 mca_type_details,
                 router_data,
                 updated_customer,
+                tokenization_action,
             )
             .await?;
 
@@ -4387,6 +4389,7 @@ pub async fn call_connector_service<F, RouterDReq, ApiRequest, D>(
     merchant_connector_account_type_details: domain::MerchantConnectorAccountTypeDetails,
     mut router_data: RouterData<F, RouterDReq, router_types::PaymentsResponseData>,
     updated_customer: Option<storage::CustomerUpdate>,
+    tokenization_action: TokenizationAction,
 ) -> RouterResult<RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
 where
     F: Send + Clone + Sync,
@@ -4416,7 +4419,20 @@ where
         &mut router_data,
         &call_connector_action,
     );
-
+    let payment_method_token_response = router_data
+        .add_payment_method_token(
+            state,
+            &connector,
+            &tokenization_action,
+            should_continue_further,
+        )
+        .await?;
+    let should_continue_further = tokenization::update_router_data_with_payment_method_token_result(
+        payment_method_token_response,
+        &mut router_data,
+        is_retry_payment,
+        should_continue_further,
+    );
     let should_continue = match router_data
         .create_order_at_connector(state, &connector, should_continue_further)
         .await?
@@ -4517,6 +4533,7 @@ pub async fn call_connector_service_prerequisites<F, RouterDReq, ApiRequest, D>(
     domain::MerchantConnectorAccountTypeDetails,
     Option<storage::CustomerUpdate>,
     RouterData<F, RouterDReq, router_types::PaymentsResponseData>,
+    TokenizationAction,
 )>
 where
     F: Send + Clone + Sync,
@@ -4572,10 +4589,16 @@ where
         )
         .await?;
 
+    let tokenization_action = operation
+        .to_domain()?
+        .get_connector_tokenization_action(state, payment_data)
+        .await?;
+
     Ok((
         merchant_connector_account_type_details,
         updated_customer,
         router_data,
+        tokenization_action,
     ))
 }
 
@@ -4640,25 +4663,29 @@ where
             .await?,
         ));
 
-    let (merchant_connector_account_type_details, updated_customer, router_data) =
-        call_connector_service_prerequisites(
-            state,
-            req_state,
-            merchant_context,
-            connector,
-            operation,
-            payment_data,
-            customer,
-            call_connector_action,
-            schedule_time,
-            header_payload,
-            frm_suggestion,
-            business_profile,
-            is_retry_payment,
-            should_retry_with_pan,
-            all_keys_required,
-        )
-        .await?;
+    let (
+        merchant_connector_account_type_details,
+        updated_customer,
+        router_data,
+        _tokenization_action,
+    ) = call_connector_service_prerequisites(
+        state,
+        req_state,
+        merchant_context,
+        connector,
+        operation,
+        payment_data,
+        customer,
+        call_connector_action,
+        schedule_time,
+        header_payload,
+        frm_suggestion,
+        business_profile,
+        is_retry_payment,
+        should_retry_with_pan,
+        all_keys_required,
+    )
+    .await?;
     Ok((
         merchant_connector_account_type_details,
         external_vault_merchant_connector_account_type_details,
@@ -4884,6 +4911,7 @@ pub async fn decide_unified_connector_service_call<F, RouterDReq, ApiRequest, D>
     merchant_connector_account_type_details: domain::MerchantConnectorAccountTypeDetails,
     mut router_data: RouterData<F, RouterDReq, router_types::PaymentsResponseData>,
     updated_customer: Option<storage::CustomerUpdate>,
+    tokenization_action: TokenizationAction,
 ) -> RouterResult<RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
 where
     F: Send + Clone + Sync,
@@ -4975,6 +5003,7 @@ where
                 merchant_connector_account_type_details,
                 router_data,
                 updated_customer,
+                tokenization_action,
             )
             .await
         }
@@ -6892,6 +6921,48 @@ where
     Ok(merchant_connector_account)
 }
 
+#[cfg(feature = "v2")]
+fn is_payment_method_tokenization_enabled_for_connector(
+    state: &SessionState,
+    connector_name: &str,
+    payment_method: storage::enums::PaymentMethod,
+    payment_method_type: Option<storage::enums::PaymentMethodType>,
+    mandate_flow_enabled: storage_enums::FutureUsage,
+) -> RouterResult<bool> {
+    let connector_tokenization_filter = state.conf.tokenization.0.get(connector_name);
+
+    Ok(connector_tokenization_filter
+        .map(|connector_filter| {
+            connector_filter
+                .payment_method
+                .clone()
+                .contains(&payment_method)
+                && is_payment_method_type_allowed_for_connector(
+                    payment_method_type,
+                    connector_filter.payment_method_type.clone(),
+                )
+                && is_payment_flow_allowed_for_connector(
+                    mandate_flow_enabled,
+                    connector_filter.flow.clone(),
+                )
+        })
+        .unwrap_or(false))
+}
+// Determines connector tokenization eligibility: if no flow restriction, allow for one-off/CIT with raw cards; if flow = “mandates”, only allow MIT off-session with stored tokens.
+#[cfg(feature = "v2")]
+fn is_payment_flow_allowed_for_connector(
+    mandate_flow_enabled: storage_enums::FutureUsage,
+    payment_flow: Option<PaymentFlow>,
+) -> bool {
+    if payment_flow.is_none() {
+        true
+    } else {
+        matches!(payment_flow, Some(PaymentFlow::Mandates))
+            && matches!(mandate_flow_enabled, storage_enums::FutureUsage::OffSession)
+    }
+}
+
+#[cfg(feature = "v1")]
 fn is_payment_method_tokenization_enabled_for_connector(
     state: &SessionState,
     connector_name: &str,
@@ -6924,7 +6995,7 @@ fn is_payment_method_tokenization_enabled_for_connector(
         })
         .unwrap_or(false))
 }
-
+#[cfg(feature = "v1")]
 fn is_payment_flow_allowed_for_connector(
     mandate_flow_enabled: Option<storage_enums::FutureUsage>,
     payment_flow: Option<PaymentFlow>,
@@ -7125,6 +7196,7 @@ fn is_payment_method_type_allowed_for_connector(
     }
 }
 
+#[cfg(feature = "v1")]
 #[allow(clippy::too_many_arguments)]
 async fn decide_payment_method_tokenize_action(
     state: &SessionState,
@@ -7194,7 +7266,7 @@ pub struct GooglePayPaymentProcessingDetails {
     pub google_pay_root_signing_keys: Secret<String>,
     pub google_pay_recipient_id: Secret<String>,
 }
-
+#[cfg(feature = "v1")]
 #[derive(Clone, Debug)]
 pub enum TokenizationAction {
     TokenizeInRouter,
@@ -7205,23 +7277,10 @@ pub enum TokenizationAction {
 }
 
 #[cfg(feature = "v2")]
-#[allow(clippy::too_many_arguments)]
-pub async fn get_connector_tokenization_action_when_confirm_true<F, Req, D>(
-    _state: &SessionState,
-    _operation: &BoxedOperation<'_, F, Req, D>,
-    payment_data: &mut D,
-    _validate_result: &operations::ValidateResult,
-    _merchant_key_store: &domain::MerchantKeyStore,
-    _customer: &Option<domain::Customer>,
-    _business_profile: &domain::Profile,
-) -> RouterResult<(D, TokenizationAction)>
-where
-    F: Send + Clone,
-    D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
-{
-    // TODO: Implement this function
-    let payment_data = payment_data.to_owned();
-    Ok((payment_data, TokenizationAction::SkipConnectorTokenization))
+#[derive(Clone, Debug)]
+pub enum TokenizationAction {
+    TokenizeInConnector,
+    SkipConnectorTokenization,
 }
 
 #[cfg(feature = "v1")]
diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs
index 2d8488ab6cc..9617910ed41 100644
--- a/crates/router/src/core/payments/operations.rs
+++ b/crates/router/src/core/payments/operations.rs
@@ -91,6 +91,8 @@ pub use self::{
     payment_session_intent::PaymentSessionIntent,
 };
 use super::{helpers, CustomerDetails, OperationSessionGetters, OperationSessionSetters};
+#[cfg(feature = "v2")]
+use crate::core::payments;
 use crate::{
     core::errors::{self, CustomResult, RouterResult},
     routes::{app::ReqState, SessionState},
@@ -423,6 +425,16 @@ pub trait Domain<F: Clone, R, D>: Send + Sync {
         Ok(())
     }
 
+    /// Get connector tokenization action
+    #[cfg(feature = "v2")]
+    async fn get_connector_tokenization_action<'a>(
+        &'a self,
+        _state: &SessionState,
+        _payment_data: &D,
+    ) -> RouterResult<(payments::TokenizationAction)> {
+        Ok(payments::TokenizationAction::SkipConnectorTokenization)
+    }
+
     // #[cfg(feature = "v2")]
     // async fn call_connector<'a, RouterDataReq>(
     //     &'a self,
diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs
index ccbe540dfc5..9720cee1d9a 100644
--- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs
+++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs
@@ -538,6 +538,60 @@ impl<F: Clone + Send + Sync> Domain<F, PaymentsConfirmIntentRequest, PaymentConf
             .set_connector_in_payment_attempt(Some(connector_data.connector_name.to_string()));
         Ok(connector_data)
     }
+
+    async fn get_connector_tokenization_action<'a>(
+        &'a self,
+        state: &SessionState,
+        payment_data: &PaymentConfirmData<F>,
+    ) -> RouterResult<payments::TokenizationAction> {
+        let connector = payment_data.payment_attempt.connector.to_owned();
+
+        let is_connector_mandate_flow = payment_data
+            .mandate_data
+            .as_ref()
+            .and_then(|mandate_details| mandate_details.mandate_reference_id.as_ref())
+            .map(|mandate_reference| match mandate_reference {
+                api_models::payments::MandateReferenceId::ConnectorMandateId(_) => true,
+                api_models::payments::MandateReferenceId::NetworkMandateId(_)
+                | api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_) => false,
+            })
+            .unwrap_or(false);
+
+        let tokenization_action = match connector {
+            Some(_) if is_connector_mandate_flow => {
+                payments::TokenizationAction::SkipConnectorTokenization
+            }
+            Some(connector) => {
+                let payment_method = payment_data
+                    .payment_attempt
+                    .get_payment_method()
+                    .ok_or_else(|| errors::ApiErrorResponse::InternalServerError)
+                    .attach_printable("Payment method not found")?;
+                let payment_method_type: Option<common_enums::PaymentMethodType> =
+                    payment_data.payment_attempt.get_payment_method_type();
+
+                let mandate_flow_enabled = payment_data.payment_intent.setup_future_usage;
+
+                let is_connector_tokenization_enabled =
+                    payments::is_payment_method_tokenization_enabled_for_connector(
+                        state,
+                        &connector,
+                        payment_method,
+                        payment_method_type,
+                        mandate_flow_enabled,
+                    )?;
+
+                if is_connector_tokenization_enabled {
+                    payments::TokenizationAction::TokenizeInConnector
+                } else {
+                    payments::TokenizationAction::SkipConnectorTokenization
+                }
+            }
+            None => payments::TokenizationAction::SkipConnectorTokenization,
+        };
+
+        Ok(tokenization_action)
+    }
 }
 
 #[async_trait]

## 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 -->
This feature add connector-level tokenization to Hyperswitch V2, allowing secure storage of card data as tokens at payment connectors like Braintree.
Additionally Tokenization in router is not implemented, because of temp locker mechanism which is still pending.  

For connector-level tokenization:
1.When a payment comes with raw card data and the connector supports tokenization, send the card details to the connector’s tokenization API.
2. Receive the tokenized payment method ID.
3. Use the token for the subsequent payment call.


### 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).
-->
Closes #9501 



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

<details>
<summary>▶ Create and Confirm Request</summary>

**Request:**
```bash
curl --location 'http://localhost:8080/v2/payments' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: pro_pYmCUDflXtfYghJEbwig' \
--header 'Authorization: api-key=dev_NJmZgHkdmFD5DC5ldyvqUObC7LgVZAkuNSsUCxlcwTYfdMb2zxsvKZQyFJ5AdIwI' \
--data-raw '{
    "amount_details": {
        "order_amount": 100,
        "currency": "USD"
    },
    "capture_method": "automatic",
    "authentication_type": "no_three_ds",
    "billing": {
        "address": {
            "first_name": "John",
            "last_name": "Dough"
        },
        "email": "example@example.com"
    },
    "payment_method_data": {
        "card": {
            "card_number": "4111111111111111",
            "card_exp_month": "01",
            "card_exp_year": "29",
            "card_holder_name": "John Doe",
            "card_cvc": "100"
        }
    },
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "shipping": {
    "address": {
        "first_name": "John",
        "last_name": "Dough",
        "city": "Karwar",
        "zip": "581301",
        "state": "Karnataka"
    },
    "email": "example@example.com"
}
}'
```

**Response:**
```json
{
    "id": "12345_pay_0199758923487c2380efbcb7a0480045",
    "status": "succeeded",
    "amount": {
        "order_amount": 100,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 100,
        "amount_to_capture": null,
        "amount_capturable": 0,
        "amount_captured": 100
    },
    "customer_id": null,
    "connector": "braintree",
    "created": "2025-09-23T07:45:45.801Z",
    "modified_at": "2025-09-23T07:45:46.773Z",
    "payment_method_data": {
        "billing": null
    },
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "connector_transaction_id": "dHJhbnNhY3Rpb25fcWE0NGtmdHM",
    "connector_reference_id": null,
    "merchant_connector_id": "mca_15ue2HSPzmSPtuWvipkZ",
    "browser_info": null,
    "error": null,
    "shipping": null,
    "billing": null,
    "attempts": null,
    "connector_token_details": null,
    "payment_method_id": null,
    "next_action": null,
    "return_url": "https://google.com/success",
    "authentication_type": "no_three_ds",
    "authentication_type_applied": "no_three_ds",
    "is_iframe_redirection_enabled": null,
    "merchant_reference_id": null,
    "raw_connector_response": null,
    "feature_metadata": null,
    "metadata": null
}
```
</details>

---

<details>
<summary>▶ Create Intent</summary>

**Request:**
```
{
    "amount_details": {
        "order_amount": 10000,
        "currency": "USD"
    },
    "capture_method":"automatic",
    "authentication_type": "no_three_ds",
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "email": "example@example.com"
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "email": "example@example.com"
    }

}
```

**Response:**
```json
{
    "id": "12345_pay_019975899d367c61ae62b88f1edde4e9",
    "status": "requires_payment_method",
    "amount_details": {
        "order_amount": 10000,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null
    },
    "client_secret": "cs_019975899d487c93a8bf823b41c05b44",
    "profile_id": "pro_pYmCUDflXtfYghJEbwig",
    "merchant_reference_id": null,
    "routing_algorithm_id": null,
    "capture_method": "automatic",
    "authentication_type": "no_three_ds",
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "joseph",
            "last_name": "Doe",
            "origin_zip": null
        },
        "phone": null,
        "email": "example@example.com"
    },
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "joseph",
            "last_name": "Doe",
            "origin_zip": null
        },
        "phone": null,
        "email": "example@example.com"
    },
    "customer_id": null,
    "customer_present": "present",
    "description": null,
    "return_url": null,
    "setup_future_usage": "on_session",
    "apply_mit_exemption": "Skip",
    "statement_descriptor": null,
    "order_details": null,
    "allowed_payment_method_types": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "payment_link_enabled": "Skip",
    "payment_link_config": null,
    "request_incremental_authorization": "false",
    "split_txns_enabled": "skip",
    "expires_on": "2025-09-23T08:01:17.027Z",
    "frm_metadata": null,
    "request_external_three_ds_authentication": "Skip",
    "payment_type": "normal"
}
```
</details>

---

<details>
<summary>▶ Confirm Intent</summary>

**Request:**
```
curl --location 'http://localhost:8080/v2/payments/12345_pay_01997581b5657212a9e70f003b9d90b4/confirm-intent' \
--header 'x-profile-id: pro_k9nKKsBbyN4y6mOzfNe3' \
--header 'x-client-secret: cs_01997581b57e7683ae1c09b731afd73c' \
--header 'Authorization: api-key=dev_RDL3XpNVl4roIIGE1841URGxogUhALsFsxybasIi5yX62pvgFh4A3SNEWrKGvvBX' \
--header 'Content-Type: application/json' \
--header 'api-key: pk_dev_e02152d7d0c542dc934d95388df8a940' \
--data '{
    "payment_method_data": {
        "card": {
            "card_number": "4242424242424242",
            "card_exp_month": "01",
            "card_exp_year": "26",
            "card_holder_name": "John Doe",
            "card_cvc": "100"
        }
    },
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
     "browser_info": {
        "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
        "accept_header": "text\\/html,application\\/xhtml+xml,application\\/xml;q=0.9,image\\/webp,image\\/apng,*\\/*;q=0.8",
        "language": "en-GB",
        "color_depth": 24,
        "screen_height": 1440,
        "screen_width": 2560,
        "time_zone": -330,
        "java_enabled": true,
        "java_script_enabled": true,
        "ip_address": "0.0.0.0"
    }
}'
```

**Response:**
```json
{
    "id": "12345_pay_019975899d367c61ae62b88f1edde4e9",
    "status": "succeeded",
    "amount": {
        "order_amount": 10000,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 10000,
        "amount_to_capture": null,
        "amount_capturable": 0,
        "amount_captured": 10000
    },
    "customer_id": null,
    "connector": "braintree",
    "created": "2025-09-23T07:46:17.027Z",
    "modified_at": "2025-09-23T07:47:00.359Z",
    "payment_method_data": {
        "billing": null
    },
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "connector_transaction_id": "dHJhbnNhY3Rpb25fZnhxaDczNXY",
    "connector_reference_id": null,
    "merchant_connector_id": "mca_15ue2HSPzmSPtuWvipkZ",
    "browser_info": null,
    "error": null,
    "shipping": null,
    "billing": null,
    "attempts": null,
    "connector_token_details": null,
    "payment_method_id": null,
    "next_action": null,
    "return_url": "https://google.com/success",
    "authentication_type": "no_three_ds",
    "authentication_type_applied": "no_three_ds",
    "is_iframe_redirection_enabled": null,
    "merchant_reference_id": null,
    "raw_connector_response": null,
    "feature_metadata": null,
    "metadata": null
}
```
</details>

<img width="1728" height="428" alt="image" src="https://github.com/user-attachments/assets/84ef20bd-fbbd-43ab-8733-07ec7d693c5b" />


<img width="1727" height="452" alt="image" src="https://github.com/user-attachments/assets/fb66e5f9-95d8-4a1a-bc68-c90809f8b5e0" />


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