From efab34f0ef0bd032b049778f18f3cb688faa7fa7 Mon Sep 17 00:00:00 2001 From: Ayush Anand <114248859+ayush22667@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:56:58 +0530 Subject: [PATCH] feat(payments): add tokenization action handling to payment flow for braintree (#9506) --- crates/router/src/core/payments.rs | 141 +++++++++++++----- crates/router/src/core/payments/operations.rs | 12 ++ .../operations/payment_confirm_intent.rs | 54 +++++++ 3 files changed, 166 insertions(+), 41 deletions(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 72cf0b80bd..e9a917153f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -224,7 +224,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(), @@ -264,6 +264,7 @@ where mca_type_details, router_data, updated_customer, + tokenization_action, ) .await?; @@ -304,7 +305,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(), @@ -344,6 +345,7 @@ where mca_type_details, router_data, updated_customer, + tokenization_action, ) .await?; @@ -4397,6 +4399,7 @@ pub async fn call_connector_service( merchant_connector_account_type_details: domain::MerchantConnectorAccountTypeDetails, mut router_data: RouterData, updated_customer: Option, + tokenization_action: TokenizationAction, ) -> RouterResult> where F: Send + Clone + Sync, @@ -4426,7 +4429,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? @@ -4527,6 +4543,7 @@ pub async fn call_connector_service_prerequisites( domain::MerchantConnectorAccountTypeDetails, Option, RouterData, + TokenizationAction, )> where F: Send + Clone + Sync, @@ -4582,10 +4599,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, )) } @@ -4650,25 +4673,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, @@ -4897,6 +4924,7 @@ pub async fn decide_unified_connector_service_call merchant_connector_account_type_details: domain::MerchantConnectorAccountTypeDetails, mut router_data: RouterData, updated_customer: Option, + tokenization_action: TokenizationAction, ) -> RouterResult> where F: Send + Clone + Sync, @@ -4993,6 +5021,7 @@ where merchant_connector_account_type_details, router_data, updated_customer, + tokenization_action, ) .await } @@ -6938,6 +6967,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, + mandate_flow_enabled: storage_enums::FutureUsage, +) -> RouterResult { + 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, +) -> 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, @@ -6975,7 +7046,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, payment_flow: Option, @@ -7198,6 +7269,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, @@ -7267,7 +7339,7 @@ pub struct GooglePayPaymentProcessingDetails { pub google_pay_root_signing_keys: Secret, pub google_pay_recipient_id: Secret, } - +#[cfg(feature = "v1")] #[derive(Clone, Debug)] pub enum TokenizationAction { TokenizeInRouter, @@ -7278,23 +7350,10 @@ pub enum TokenizationAction { } #[cfg(feature = "v2")] -#[allow(clippy::too_many_arguments)] -pub async fn get_connector_tokenization_action_when_confirm_true( - _state: &SessionState, - _operation: &BoxedOperation<'_, F, Req, D>, - payment_data: &mut D, - _validate_result: &operations::ValidateResult, - _merchant_key_store: &domain::MerchantKeyStore, - _customer: &Option, - _business_profile: &domain::Profile, -) -> RouterResult<(D, TokenizationAction)> -where - F: Send + Clone, - D: OperationSessionGetters + OperationSessionSetters + 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 11c26b5c54..01023f2290 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -94,6 +94,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}, @@ -426,6 +428,16 @@ pub trait Domain: 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 ccbe540dfc..9720cee1d9 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 Domain( + &'a self, + state: &SessionState, + payment_data: &PaymentConfirmData, + ) -> RouterResult { + 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 = + 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]