diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 3f9167ab7578..714e12d54a9f 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -9725,7 +9725,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -9780,6 +9781,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -9864,7 +9869,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -9921,6 +9927,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -10025,6 +10035,11 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible", + "nullable": true } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 2aa19e4e223c..74c7b8e050f5 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -12315,7 +12315,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -12370,6 +12371,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -12454,7 +12459,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -12511,6 +12517,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -12615,6 +12625,11 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible", + "nullable": true } } }, @@ -22448,6 +22463,11 @@ "type": "object", "description": "Product authentication ids", "nullable": true + }, + "is_clear_pan_retries_enabled": { + "type": "boolean", + "description": "Indicates if clear pan retries is enabled or not.", + "nullable": true } }, "additionalProperties": false @@ -22481,7 +22501,8 @@ "is_tax_connector_enabled", "is_network_tokenization_enabled", "is_auto_retries_enabled", - "is_click_to_pay_enabled" + "is_click_to_pay_enabled", + "is_clear_pan_retries_enabled" ], "properties": { "merchant_id": { @@ -22689,6 +22710,10 @@ "type": "object", "description": "Product authentication ids", "nullable": true + }, + "is_clear_pan_retries_enabled": { + "type": "boolean", + "description": "Indicates if clear pan retries is enabled or not." } } }, diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index ef25a4350d95..1a4b58abaec4 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1864,6 +1864,9 @@ pub struct ProfileCreate { /// Product authentication ids #[schema(value_type = Option, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)] pub authentication_product_ids: Option>, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: Option, } #[nutype::nutype( @@ -2117,6 +2120,9 @@ pub struct ProfileResponse { /// Product authentication ids #[schema(value_type = Option, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)] pub authentication_product_ids: Option, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v2")] @@ -2370,6 +2376,9 @@ pub struct ProfileUpdate { /// Product authentication ids #[schema(value_type = Option, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)] pub authentication_product_ids: Option>, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v2")] diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index b95794ef707b..7ffc9c69413b 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -29,6 +29,8 @@ pub struct GsmCreateRequest { pub unified_message: Option, /// category in which error belongs to pub error_category: Option, + /// indicates if retry with pan is possible + pub clear_pan_possible: bool, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -93,6 +95,8 @@ pub struct GsmUpdateRequest { pub unified_message: Option, /// category in which error belongs to pub error_category: Option, + /// indicates if retry with pan is possible + pub clear_pan_possible: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -148,4 +152,6 @@ pub struct GsmResponse { pub unified_message: Option, /// category in which error belongs to pub error_category: Option, + /// indicates if retry with pan is possible + pub clear_pan_possible: bool, } diff --git a/crates/common_utils/src/ext_traits.rs b/crates/common_utils/src/ext_traits.rs index 945056aafefd..369ff4ce0800 100644 --- a/crates/common_utils/src/ext_traits.rs +++ b/crates/common_utils/src/ext_traits.rs @@ -295,28 +295,34 @@ impl StringExt for String { /// Extending functionalities of Wrapper types for idiomatic #[cfg(feature = "async_ext")] #[cfg_attr(feature = "async_ext", async_trait::async_trait)] -pub trait AsyncExt { +pub trait AsyncExt { /// Output type of the map function type WrappedSelf; /// Extending map by allowing functions which are async - async fn async_map(self, func: F) -> Self::WrappedSelf + async fn async_map(self, func: F) -> Self::WrappedSelf where F: FnOnce(A) -> Fut + Send, Fut: futures::Future + Send; /// Extending the `and_then` by allowing functions which are async - async fn async_and_then(self, func: F) -> Self::WrappedSelf + async fn async_and_then(self, func: F) -> Self::WrappedSelf where F: FnOnce(A) -> Fut + Send, Fut: futures::Future> + Send; + + /// Extending `unwrap_or_else` to allow async fallback + async fn async_unwrap_or_else(self, func: F) -> A + where + F: FnOnce() -> Fut + Send, + Fut: futures::Future + Send; } #[cfg(feature = "async_ext")] #[cfg_attr(feature = "async_ext", async_trait::async_trait)] -impl AsyncExt for Result { +impl AsyncExt for Result { type WrappedSelf = Result; - async fn async_and_then(self, func: F) -> Self::WrappedSelf + async fn async_and_then(self, func: F) -> Self::WrappedSelf where F: FnOnce(A) -> Fut + Send, Fut: futures::Future> + Send, @@ -327,7 +333,7 @@ impl AsyncExt for Result { } } - async fn async_map(self, func: F) -> Self::WrappedSelf + async fn async_map(self, func: F) -> Self::WrappedSelf where F: FnOnce(A) -> Fut + Send, Fut: futures::Future + Send, @@ -337,13 +343,24 @@ impl AsyncExt for Result { Err(err) => Err(err), } } + + async fn async_unwrap_or_else(self, func: F) -> A + where + F: FnOnce() -> Fut + Send, + Fut: futures::Future + Send, + { + match self { + Ok(a) => a, + Err(_) => func().await, + } + } } #[cfg(feature = "async_ext")] #[cfg_attr(feature = "async_ext", async_trait::async_trait)] -impl AsyncExt for Option { +impl AsyncExt for Option { type WrappedSelf = Option; - async fn async_and_then(self, func: F) -> Self::WrappedSelf + async fn async_and_then(self, func: F) -> Self::WrappedSelf where F: FnOnce(A) -> Fut + Send, Fut: futures::Future> + Send, @@ -354,7 +371,7 @@ impl AsyncExt for Option { } } - async fn async_map(self, func: F) -> Self::WrappedSelf + async fn async_map(self, func: F) -> Self::WrappedSelf where F: FnOnce(A) -> Fut + Send, Fut: futures::Future + Send, @@ -364,6 +381,17 @@ impl AsyncExt for Option { None => None, } } + + async fn async_unwrap_or_else(self, func: F) -> A + where + F: FnOnce() -> Fut + Send, + Fut: futures::Future + Send, + { + match self { + Some(a) => a, + None => func().await, + } + } } /// Extension trait for validating application configuration. This trait provides utilities to diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index dff3f174fc50..982078cb95f9 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -59,6 +59,7 @@ pub struct Profile { pub max_auto_retries_enabled: Option, pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v1")] @@ -104,6 +105,7 @@ pub struct ProfileNew { pub max_auto_retries_enabled: Option, pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v1")] @@ -146,6 +148,7 @@ pub struct ProfileUpdateInternal { pub max_auto_retries_enabled: Option, pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v1")] @@ -187,6 +190,7 @@ impl ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, + is_clear_pan_retries_enabled, } = self; Profile { profile_id: source.profile_id, @@ -250,6 +254,8 @@ impl ProfileUpdateInternal { .unwrap_or(source.is_click_to_pay_enabled), authentication_product_ids: authentication_product_ids .or(source.authentication_product_ids), + is_clear_pan_retries_enabled: is_clear_pan_retries_enabled + .or(source.is_clear_pan_retries_enabled), } } } diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index aba8d75a6ebb..46583a5b22c5 100644 --- a/crates/diesel_models/src/gsm.rs +++ b/crates/diesel_models/src/gsm.rs @@ -41,6 +41,7 @@ pub struct GatewayStatusMap { pub unified_code: Option, pub unified_message: Option, pub error_category: Option, + pub clear_pan_possible: bool, } #[derive(Clone, Debug, Eq, PartialEq, Insertable)] @@ -58,6 +59,7 @@ pub struct GatewayStatusMappingNew { pub unified_code: Option, pub unified_message: Option, pub error_category: Option, + pub clear_pan_possible: bool, } #[derive( @@ -78,6 +80,7 @@ pub struct GatewayStatusMapperUpdateInternal { pub unified_message: Option, pub error_category: Option, pub last_modified: PrimitiveDateTime, + pub clear_pan_possible: Option, } #[derive(Debug)] @@ -89,6 +92,7 @@ pub struct GatewayStatusMappingUpdate { pub unified_code: Option, pub unified_message: Option, pub error_category: Option, + pub clear_pan_possible: Option, } impl From for GatewayStatusMapperUpdateInternal { @@ -101,6 +105,7 @@ impl From for GatewayStatusMapperUpdateInternal { unified_code, unified_message, error_category, + clear_pan_possible, } = value; Self { status, @@ -116,6 +121,7 @@ impl From for GatewayStatusMapperUpdateInternal { sub_flow: None, code: None, message: None, + clear_pan_possible, } } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 95bb714cb711..c3ea98b5fd1e 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -216,6 +216,7 @@ diesel::table! { max_auto_retries_enabled -> Nullable, is_click_to_pay_enabled -> Bool, authentication_product_ids -> Nullable, + is_clear_pan_retries_enabled -> Nullable, } } @@ -544,6 +545,7 @@ diesel::table! { unified_message -> Nullable, #[max_length = 64] error_category -> Nullable, + clear_pan_possible -> Bool, } } diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index 1df474f18d5f..090a7eb352e9 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -60,6 +60,7 @@ pub struct Profile { pub max_auto_retries_enabled: Option, pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v1")] @@ -102,6 +103,7 @@ pub struct ProfileSetter { pub max_auto_retries_enabled: Option, pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v1")] @@ -151,6 +153,7 @@ impl From for Profile { max_auto_retries_enabled: value.max_auto_retries_enabled, is_click_to_pay_enabled: value.is_click_to_pay_enabled, authentication_product_ids: value.authentication_product_ids, + is_clear_pan_retries_enabled: value.is_clear_pan_retries_enabled, } } } @@ -202,6 +205,7 @@ pub struct ProfileGeneralUpdate { pub max_auto_retries_enabled: Option, pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v1")] @@ -266,6 +270,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, + is_clear_pan_retries_enabled, } = *update; Self { @@ -305,6 +310,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, + is_clear_pan_retries_enabled, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -346,6 +352,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -385,6 +392,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -424,6 +432,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -463,6 +472,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -502,6 +512,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + is_clear_pan_retries_enabled: None, }, } } @@ -560,6 +571,7 @@ impl super::behaviour::Conversion for Profile { max_auto_retries_enabled: self.max_auto_retries_enabled, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + is_clear_pan_retries_enabled: Some(self.is_clear_pan_retries_enabled), }) } @@ -630,6 +642,7 @@ impl super::behaviour::Conversion for Profile { max_auto_retries_enabled: item.max_auto_retries_enabled, is_click_to_pay_enabled: item.is_click_to_pay_enabled, authentication_product_ids: item.authentication_product_ids, + is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled.unwrap_or(false), }) } .await @@ -684,6 +697,7 @@ impl super::behaviour::Conversion for Profile { max_auto_retries_enabled: self.max_auto_retries_enabled, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + is_clear_pan_retries_enabled: Some(self.is_clear_pan_retries_enabled), }) } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 24a2b25d6ce9..48bc61924fda 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3705,6 +3705,7 @@ impl ProfileCreateBridge for api::ProfileCreate { max_auto_retries_enabled: self.max_auto_retries_enabled.map(i16::from), is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids, + is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled.unwrap_or_default(), })) } @@ -4079,6 +4080,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { max_auto_retries_enabled: self.max_auto_retries_enabled.map(i16::from), is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids, + is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, }, ))) } diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs index c7daee111a9a..6c544791a18e 100644 --- a/crates/router/src/core/gsm.rs +++ b/crates/router/src/core/gsm.rs @@ -68,6 +68,7 @@ pub async fn update_gsm_rule( unified_code, unified_message, error_category, + clear_pan_possible, } = gsm_request; GsmInterface::update_gsm_rule( db, @@ -84,6 +85,7 @@ pub async fn update_gsm_rule( unified_code, unified_message, error_category, + clear_pan_possible, }, ) .await diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 6cdadf07320c..7f817e25004b 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -24,7 +24,7 @@ use api_models::payment_methods; #[cfg(feature = "payouts")] pub use api_models::{enums::PayoutConnectors, payouts as payout_types}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use common_utils::ext_traits::Encode; +use common_utils::ext_traits::{Encode, OptionExt}; use common_utils::{consts::DEFAULT_LOCALE, id_type}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use common_utils::{ @@ -70,7 +70,7 @@ use crate::{ consts, core::{ errors::{self, RouterResult}, - payments::helpers as payment_helpers, + payments::{self, helpers as payment_helpers}, }, routes::{app::StorageInterface, SessionState}, services, @@ -535,6 +535,8 @@ pub async fn retrieve_payment_method_with_token( mandate_id: Option, payment_method_info: Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, + vault_data: Option<&payments::VaultData>, ) -> RouterResult { let token = match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { @@ -578,7 +580,7 @@ pub async fn retrieve_payment_method_with_token( } storage::PaymentTokenData::Permanent(card_token) => { - payment_helpers::retrieve_card_with_permanent_token( + payment_helpers::retrieve_payment_method_data_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), card_token @@ -590,9 +592,14 @@ pub async fn retrieve_payment_method_with_token( merchant_key_store, storage_scheme, mandate_id, - payment_method_info, + payment_method_info + .get_required_value("PaymentMethod") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("PaymentMethod not found")?, business_profile, payment_attempt.connector.clone(), + should_retry_with_pan, + vault_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card)))? @@ -613,7 +620,7 @@ pub async fn retrieve_payment_method_with_token( } storage::PaymentTokenData::PermanentCard(card_token) => { - payment_helpers::retrieve_card_with_permanent_token( + payment_helpers::retrieve_payment_method_data_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), card_token @@ -625,9 +632,14 @@ pub async fn retrieve_payment_method_with_token( merchant_key_store, storage_scheme, mandate_id, - payment_method_info, + payment_method_info + .get_required_value("PaymentMethod") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("PaymentMethod not found")?, business_profile, payment_attempt.connector.clone(), + should_retry_with_pan, + vault_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card)))? diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index cbf2cdea5384..e2606e26e9a3 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -479,6 +479,7 @@ where None, &business_profile, false, + false, ) .await?; @@ -581,6 +582,7 @@ where None, &business_profile, false, + false, ) .await?; @@ -2452,6 +2454,7 @@ pub async fn call_connector_service( frm_suggestion: Option, business_profile: &domain::Profile, is_retry_payment: bool, + should_retry_with_pan: bool, ) -> RouterResult<( RouterData, helpers::MerchantConnectorAccountType, @@ -2504,6 +2507,7 @@ where key_store, customer, business_profile, + should_retry_with_pan, ) .await?; *payment_data = pd; @@ -2704,6 +2708,7 @@ pub async fn call_connector_service( frm_suggestion: Option, business_profile: &domain::Profile, is_retry_payment: bool, + should_retry_with_pan: bool, ) -> RouterResult> where F: Send + Clone + Sync, @@ -4195,6 +4200,7 @@ pub async fn get_connector_tokenization_action_when_confirm_true( merchant_key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<(D, TokenizationAction)> where F: Send + Clone, @@ -4265,6 +4271,7 @@ where merchant_key_store, customer, business_profile, + should_retry_with_pan, ) .await?; payment_data.set_payment_method_data(payment_method_data); @@ -4284,6 +4291,7 @@ where merchant_key_store, customer, business_profile, + should_retry_with_pan, ) .await?; @@ -4367,6 +4375,7 @@ where merchant_key_store, customer, business_profile, + false, ) .await?; payment_data.set_payment_method_data(payment_method_data); @@ -4432,6 +4441,47 @@ where pub tax_data: Option, pub session_id: Option, pub service_details: Option, + pub vault_operation: Option, +} + +#[derive(Clone, serde::Serialize, Debug)] +pub enum PaymentMethodDataAction { + ExistingVaultData(VaultData), +} + +#[derive(Clone, serde::Serialize, Debug)] +pub enum VaultData { + CardVaultData(hyperswitch_domain_models::payment_method_data::Card), + NetworkTokenVaultData(hyperswitch_domain_models::payment_method_data::NetworkTokenData), + CardAndNetworkToken(Box), +} + +#[derive(Default, Clone, serde::Serialize, Debug)] +pub struct CardAndNetworkTokenData { + pub card_data: hyperswitch_domain_models::payment_method_data::Card, + pub network_token_data: hyperswitch_domain_models::payment_method_data::NetworkTokenData, +} + +impl VaultData { + pub fn get_card_vault_data( + &self, + ) -> Option { + match self { + Self::CardVaultData(card_data) => Some(card_data.clone()), + Self::NetworkTokenVaultData(_network_token_data) => None, + Self::CardAndNetworkToken(vault_data) => Some(vault_data.card_data.clone()), + } + } + + pub fn get_network_token_data( + &self, + ) -> Option { + match self { + Self::CardVaultData(_card_data) => None, + Self::NetworkTokenVaultData(network_token_data) => Some(network_token_data.clone()), + Self::CardAndNetworkToken(vault_data) => Some(vault_data.network_token_data.clone()), + } + } } #[derive(Clone, serde::Serialize, Debug)] @@ -6541,7 +6591,6 @@ pub async fn payment_external_authentication( &payment_intent, &key_store, storage_scheme, - &business_profile, ) .await? .ok_or(errors::ApiErrorResponse::InternalServerError) @@ -6901,6 +6950,7 @@ pub trait OperationSessionGetters { fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails>; fn get_force_sync(&self) -> Option; fn get_capture_method(&self) -> Option; + fn get_vault_operation(&self) -> Option<&PaymentMethodDataAction>; #[cfg(feature = "v2")] fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt>; @@ -6946,6 +6996,7 @@ pub trait OperationSessionSetters { straight_through_algorithm: serde_json::Value, ); fn set_connector_in_payment_attempt(&mut self, connector: Option); + fn set_vault_operation(&mut self, vault_operation: PaymentMethodDataAction); } #[cfg(feature = "v1")] @@ -7078,6 +7129,10 @@ impl OperationSessionGetters for PaymentData { self.payment_attempt.capture_method } + fn get_vault_operation(&self) -> Option<&PaymentMethodDataAction> { + self.vault_operation.as_ref() + } + // #[cfg(feature = "v2")] // fn get_capture_method(&self) -> Option { // Some(self.payment_intent.capture_method) @@ -7194,6 +7249,10 @@ impl OperationSessionSetters for PaymentData { fn set_connector_in_payment_attempt(&mut self, connector: Option) { self.payment_attempt.connector = connector; } + + fn set_vault_operation(&mut self, vault_operation: PaymentMethodDataAction) { + self.vault_operation = Some(vault_operation); + } } #[cfg(feature = "v2")] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index eda869c3a6ca..72599ce2eed4 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1904,12 +1904,93 @@ pub async fn retrieve_card_with_permanent_token( todo!() } +pub enum VaultFetchAction { + FetchCardDetailsFromLocker, + FetchCardDetailsForNetworkTransactionFlowFromLocker, + FetchNetworkTokenDataFromTokenizationService(String), + FetchNetworkTokenDetailsFromLocker(api_models::payments::NetworkTokenWithNTIRef), + NoFetchAction, +} + +pub fn decide_payment_method_retrieval_action( + is_network_tokenization_enabled: bool, + mandate_id: Option, + connector: Option, + network_tokenization_supported_connectors: &std::collections::HashSet, + should_retry_with_pan: bool, + network_token_requestor_ref_id: Option, +) -> VaultFetchAction { + let normal_flow = || { + determine_standard_vault_action( + is_network_tokenization_enabled, + mandate_id, + connector, + network_tokenization_supported_connectors, + network_token_requestor_ref_id, + ) + }; + + should_retry_with_pan + .then_some(VaultFetchAction::FetchCardDetailsFromLocker) + .unwrap_or_else(normal_flow) +} + +pub fn determine_standard_vault_action( + is_network_tokenization_enabled: bool, + mandate_id: Option, + connector: Option, + network_tokenization_supported_connectors: &std::collections::HashSet, + network_token_requestor_ref_id: Option, +) -> VaultFetchAction { + let is_network_transaction_id_flow = mandate_id + .as_ref() + .map(|mandate_ids| mandate_ids.is_network_transaction_id_flow()) + .unwrap_or(false); + + if !is_network_tokenization_enabled { + if is_network_transaction_id_flow { + VaultFetchAction::FetchCardDetailsForNetworkTransactionFlowFromLocker + } else { + VaultFetchAction::FetchCardDetailsFromLocker + } + } else { + match mandate_id { + Some(mandate_ids) => match mandate_ids.mandate_reference_id { + Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI(nt_data)) => { + VaultFetchAction::FetchNetworkTokenDetailsFromLocker(nt_data) + } + Some(api_models::payments::MandateReferenceId::NetworkMandateId(_)) => { + VaultFetchAction::FetchCardDetailsForNetworkTransactionFlowFromLocker + } + Some(api_models::payments::MandateReferenceId::ConnectorMandateId(_)) | None => { + VaultFetchAction::NoFetchAction + } + }, + None => { + //save card flow + let is_supported_connector = connector + .map(|conn| network_tokenization_supported_connectors.contains(&conn)) + .unwrap_or(false); + + match (is_supported_connector, network_token_requestor_ref_id) { + (true, Some(ref_id)) => { + VaultFetchAction::FetchNetworkTokenDataFromTokenizationService(ref_id) + } + (false, Some(_)) | (true, None) | (false, None) => { + VaultFetchAction::FetchCardDetailsFromLocker + } + } + } + } + } +} + #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") ))] #[allow(clippy::too_many_arguments)] -pub async fn retrieve_card_with_permanent_token( +pub async fn retrieve_payment_method_data_with_permanent_token( state: &SessionState, locker_id: &str, _payment_method_id: &str, @@ -1918,9 +1999,11 @@ pub async fn retrieve_card_with_permanent_token( _merchant_key_store: &domain::MerchantKeyStore, _storage_scheme: enums::MerchantStorageScheme, mandate_id: Option, - payment_method_info: Option, + payment_method_info: domain::PaymentMethod, business_profile: &domain::Profile, connector: Option, + should_retry_with_pan: bool, + vault_data: Option<&payments::VaultData>, ) -> RouterResult { let customer_id = payment_intent .customer_id @@ -1930,238 +2013,172 @@ pub async fn retrieve_card_with_permanent_token( message: "no customer id provided for the payment".to_string(), })?; - if !business_profile.is_network_tokenization_enabled { - let is_network_transaction_id_flow = mandate_id - .map(|mandate_ids| mandate_ids.is_network_transaction_id_flow()) - .unwrap_or(false); + let network_tokenization_supported_connectors = &state + .conf + .network_tokenization_supported_connectors + .connector_list; - if is_network_transaction_id_flow { - let card_details_from_locker = cards::get_card_from_locker( - state, - customer_id, - &payment_intent.merchant_id, - locker_id, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch card details from locker")?; - - let card_network = card_details_from_locker - .card_brand - .map(|card_brand| enums::CardNetwork::from_str(&card_brand)) - .transpose() - .map_err(|e| { - logger::error!("Failed to parse card network {e:?}"); + let connector_variant = connector + .as_ref() + .map(|conn| { + api_enums::Connector::from_str(conn.as_str()) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", }) - .ok() - .flatten(); + .attach_printable_lazy(|| format!("unable to parse connector name {connector:?}")) + }) + .transpose()?; - let card_details_for_network_transaction_id = hyperswitch_domain_models::payment_method_data::CardDetailsForNetworkTransactionId { - card_number: card_details_from_locker.card_number, - card_exp_month: card_details_from_locker.card_exp_month, - card_exp_year: card_details_from_locker.card_exp_year, - card_issuer: None, - card_network, - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: card_details_from_locker.nick_name.map(masking::Secret::new), - card_holder_name: card_details_from_locker.name_on_card.clone(), - }; + let vault_fetch_action = decide_payment_method_retrieval_action( + business_profile.is_network_tokenization_enabled, + mandate_id, + connector_variant, + network_tokenization_supported_connectors, + should_retry_with_pan, + payment_method_info + .network_token_requestor_reference_id + .clone(), + ); + match vault_fetch_action { + VaultFetchAction::FetchCardDetailsFromLocker => { + let card_result = vault_data + .and_then(|vd| vd.get_card_vault_data()) + .map(Ok) + .async_unwrap_or_else(|| async { + fetch_card_details_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + locker_id, + card_token_data, + ) + .await + }) + .await?; - Ok( - domain::PaymentMethodData::CardDetailsForNetworkTransactionId( - card_details_for_network_transaction_id, - ), - ) - } else { - fetch_card_details_from_locker( + Ok(domain::PaymentMethodData::Card(card_result)) + } + VaultFetchAction::FetchCardDetailsForNetworkTransactionFlowFromLocker => { + fetch_card_details_for_network_transaction_flow_from_locker( state, customer_id, &payment_intent.merchant_id, locker_id, - card_token_data, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed to fetch card information from the permanent locker") } - } else { - match (payment_method_info, mandate_id) { - (None, _) => Err(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Payment method data is not present"), - (Some(ref pm_data), None) => { - // Regular (non-mandate) Payment flow - let network_tokenization_supported_connectors = &state - .conf - .network_tokenization_supported_connectors - .connector_list; - let connector_variant = connector - .as_ref() - .map(|conn| { - api_enums::Connector::from_str(conn.as_str()) - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "connector", - }) - .attach_printable_lazy(|| { - format!("unable to parse connector name {connector:?}") - }) - }) - .transpose()?; - if let (Some(_conn), Some(token_ref)) = ( - connector_variant - .filter(|conn| network_tokenization_supported_connectors.contains(conn)), - pm_data.network_token_requestor_reference_id.clone(), - ) { - logger::info!("Fetching network token data from tokenization service"); - match network_tokenization::get_token_from_tokenization_service( - state, token_ref, pm_data, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "failed to fetch network token data from tokenization service", - ) { - Ok(network_token_data) => { - Ok(domain::PaymentMethodData::NetworkToken(network_token_data)) - } - Err(err) => { - logger::info!("Failed to fetch network token data from tokenization service {err:?}"); - logger::info!("Falling back to fetch card details from locker"); - fetch_card_details_from_locker( - state, - customer_id, - &payment_intent.merchant_id, - locker_id, - card_token_data, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "failed to fetch card information from the permanent locker", - ) - } - } - } else { - logger::info!("Either the connector is not in the NT supported list or token requestor reference ID is absent"); - logger::info!("Falling back to fetch card details from locker"); - fetch_card_details_from_locker( - state, - customer_id, - &payment_intent.merchant_id, - locker_id, - card_token_data, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch card information from the permanent locker") + VaultFetchAction::FetchNetworkTokenDataFromTokenizationService( + network_token_requestor_ref_id, + ) => { + logger::info!("Fetching network token data from tokenization service"); + match network_tokenization::get_token_from_tokenization_service( + state, + network_token_requestor_ref_id, + &payment_method_info, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch network token data from tokenization service") + { + Ok(network_token_data) => { + Ok(domain::PaymentMethodData::NetworkToken(network_token_data)) } - } - (Some(ref pm_data), Some(mandate_ids)) => { - // Mandate Payment flow - match mandate_ids.mandate_reference_id { - Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI( - nt_data, - )) => { - { - if let Some(network_token_locker_id) = - pm_data.network_token_locker_id.as_ref() - { - let mut token_data = cards::get_card_from_locker( + Err(err) => { + logger::info!( + "Failed to fetch network token data from tokenization service {err:?}" + ); + logger::info!("Falling back to fetch card details from locker"); + Ok(domain::PaymentMethodData::Card( + vault_data + .and_then(|vd| vd.get_card_vault_data()) + .map(Ok) + .async_unwrap_or_else(|| async { + fetch_card_details_from_locker( state, customer_id, &payment_intent.merchant_id, - network_token_locker_id, + locker_id, + card_token_data, ) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "failed to fetch network token information from the permanent locker", - )?; - let expiry = nt_data.token_exp_month.zip(nt_data.token_exp_year); - if let Some((exp_month, exp_year)) = expiry { - token_data.card_exp_month = exp_month; - token_data.card_exp_year = exp_year; - } - let network_token_data = domain::NetworkTokenData { - token_number: token_data.card_number, - token_cryptogram: None, - token_exp_month: token_data.card_exp_month, - token_exp_year: token_data.card_exp_year, - nick_name: token_data.nick_name.map(masking::Secret::new), - card_issuer: None, - card_network: None, - card_type: None, - card_issuing_country: None, - bank_code: None, - eci: None, - }; - Ok(domain::PaymentMethodData::NetworkToken(network_token_data)) - } else { - // Mandate but network token locker id is not present - Err(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Network token locker id is not present") - } - } - } - - Some(api_models::payments::MandateReferenceId::NetworkMandateId(_)) => { - let card_details_from_locker = cards::get_card_from_locker( + }) + .await?, + )) + } + } + } + VaultFetchAction::FetchNetworkTokenDetailsFromLocker(nt_data) => { + if let Some(network_token_locker_id) = + payment_method_info.network_token_locker_id.as_ref() + { + let network_token_data = vault_data + .and_then(|vd| vd.get_network_token_data()) + .map(Ok) + .async_unwrap_or_else(|| async { + fetch_network_token_details_from_locker( state, customer_id, &payment_intent.merchant_id, - locker_id, + network_token_locker_id, + nt_data, ) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch card details from locker")?; - - let card_network = card_details_from_locker - .card_brand - .map(|card_brand| enums::CardNetwork::from_str(&card_brand)) - .transpose() - .map_err(|e| { - logger::error!("Failed to parse card network {e:?}"); - }) - .ok() - .flatten(); - - let card_details_for_network_transaction_id = hyperswitch_domain_models::payment_method_data::CardDetailsForNetworkTransactionId { - card_number: card_details_from_locker.card_number, - card_exp_month: card_details_from_locker.card_exp_month, - card_exp_year: card_details_from_locker.card_exp_year, - card_issuer: None, - card_network, - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: card_details_from_locker.nick_name.map(masking::Secret::new), - card_holder_name: card_details_from_locker.name_on_card, - }; - - Ok( - domain::PaymentMethodData::CardDetailsForNetworkTransactionId( - card_details_for_network_transaction_id, - ), - ) - } - - Some(api_models::payments::MandateReferenceId::ConnectorMandateId(_)) - | None => Err(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Payment method data is not present"), - } + }) + .await?; + Ok(domain::PaymentMethodData::NetworkToken(network_token_data)) + } else { + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Network token locker id is not present") } } + VaultFetchAction::NoFetchAction => Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Payment method data is not present"), } } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] +#[allow(clippy::too_many_arguments)] +pub async fn retrieve_card_with_permanent_token_for_external_authentication( + state: &SessionState, + locker_id: &str, + payment_intent: &PaymentIntent, + card_token_data: Option<&domain::CardToken>, + _merchant_key_store: &domain::MerchantKeyStore, + _storage_scheme: enums::MerchantStorageScheme, +) -> RouterResult { + let customer_id = payment_intent + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "no customer id provided for the payment".to_string(), + })?; + Ok(domain::PaymentMethodData::Card( + fetch_card_details_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + locker_id, + card_token_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?, + )) +} + pub async fn fetch_card_details_from_locker( state: &SessionState, customer_id: &id_type::CustomerId, merchant_id: &id_type::MerchantId, locker_id: &str, card_token_data: Option<&domain::CardToken>, -) -> RouterResult { +) -> RouterResult { let card = cards::get_card_from_locker(state, customer_id, merchant_id, locker_id) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -2206,7 +2223,85 @@ pub async fn fetch_card_details_from_locker( card_issuing_country: None, bank_code: None, }; - Ok(domain::PaymentMethodData::Card(api_card.into())) + Ok(api_card.into()) +} + +pub async fn fetch_network_token_details_from_locker( + state: &SessionState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + network_token_locker_id: &str, + nt_data: api_models::payments::NetworkTokenWithNTIRef, +) -> RouterResult { + let mut token_data = + cards::get_card_from_locker(state, customer_id, merchant_id, network_token_locker_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "failed to fetch network token information from the permanent locker", + )?; + let expiry = nt_data.token_exp_month.zip(nt_data.token_exp_year); + if let Some((exp_month, exp_year)) = expiry { + token_data.card_exp_month = exp_month; + token_data.card_exp_year = exp_year; + } + let network_token_data = domain::NetworkTokenData { + token_number: token_data.card_number, + token_cryptogram: None, + token_exp_month: token_data.card_exp_month, + token_exp_year: token_data.card_exp_year, + nick_name: token_data.nick_name.map(masking::Secret::new), + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + eci: None, + }; + Ok(network_token_data) +} + +pub async fn fetch_card_details_for_network_transaction_flow_from_locker( + state: &SessionState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + locker_id: &str, +) -> RouterResult { + let card_details_from_locker = + cards::get_card_from_locker(state, customer_id, merchant_id, locker_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card details from locker")?; + + let card_network = card_details_from_locker + .card_brand + .map(|card_brand| enums::CardNetwork::from_str(&card_brand)) + .transpose() + .map_err(|e| { + logger::error!("Failed to parse card network {e:?}"); + }) + .ok() + .flatten(); + + let card_details_for_network_transaction_id = + hyperswitch_domain_models::payment_method_data::CardDetailsForNetworkTransactionId { + card_number: card_details_from_locker.card_number, + card_exp_month: card_details_from_locker.card_exp_month, + card_exp_year: card_details_from_locker.card_exp_year, + card_issuer: None, + card_network, + card_type: None, + card_issuing_country: None, + bank_code: None, + nick_name: card_details_from_locker.nick_name.map(masking::Secret::new), + card_holder_name: card_details_from_locker.name_on_card.clone(), + }; + + Ok( + domain::PaymentMethodData::CardDetailsForNetworkTransactionId( + card_details_for_network_transaction_id, + ), + ) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -2340,6 +2435,7 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") ))] +#[allow(clippy::too_many_arguments)] pub async fn make_pm_data<'a, F: Clone, R, D>( operation: BoxedOperation<'a, F, R, D>, state: &'a SessionState, @@ -2348,11 +2444,15 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( customer: &Option, storage_scheme: common_enums::enums::MerchantStorageScheme, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( BoxedOperation<'a, F, R, D>, Option, Option, )> { + use super::OperationSessionSetters; + use crate::core::payments::OperationSessionGetters; + let request = payment_data.payment_method_data.clone(); let mut card_token_data = payment_data @@ -2400,6 +2500,12 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( // TODO: Handle case where payment method and token both are present in request properly. let (payment_method, pm_id) = match (&request, payment_data.token_data.as_ref()) { (_, Some(hyperswitch_token)) => { + let current_payment_method_data = payment_data.get_vault_operation(); + + let vd = current_payment_method_data.map(|data| match data { + payments::PaymentMethodDataAction::ExistingVaultData(vd) => vd, + }); + let pm_data = Box::pin(payment_methods::retrieve_payment_method_with_token( state, merchant_key_store, @@ -2412,11 +2518,64 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( mandate_id, payment_data.payment_method_info.clone(), business_profile, + should_retry_with_pan, + vd, )) .await; let payment_method_details = pm_data.attach_printable("in 'make_pm_data'")?; + if let Some(ref payment_method_data) = payment_method_details.payment_method_data { + let updated_vault_operation = + match (current_payment_method_data, payment_method_data) { + (None, domain::PaymentMethodData::Card(card)) => { + Some(payments::PaymentMethodDataAction::ExistingVaultData( + payments::VaultData::CardVaultData(card.clone()), + )) + } + (None, domain::PaymentMethodData::NetworkToken(nt_data)) => { + Some(payments::PaymentMethodDataAction::ExistingVaultData( + payments::VaultData::NetworkTokenVaultData(nt_data.clone()), + )) + } + ( + Some(payments::PaymentMethodDataAction::ExistingVaultData(vault_data)), + payment_method, + ) => match (vault_data, payment_method) { + ( + payments::VaultData::CardVaultData(card), + domain::PaymentMethodData::NetworkToken(nt_data), + ) => Some(payments::PaymentMethodDataAction::ExistingVaultData( + payments::VaultData::CardAndNetworkToken(Box::new( + payments::CardAndNetworkTokenData { + card_data: card.clone(), + network_token_data: nt_data.clone(), + }, + )), + )), + ( + payments::VaultData::NetworkTokenVaultData(nt_data), + domain::PaymentMethodData::Card(card), + ) => Some(payments::PaymentMethodDataAction::ExistingVaultData( + payments::VaultData::CardAndNetworkToken(Box::new( + payments::CardAndNetworkTokenData { + card_data: card.clone(), + network_token_data: nt_data.clone(), + }, + )), + )), + _ => Some(payments::PaymentMethodDataAction::ExistingVaultData( + vault_data.clone(), + )), + }, + _ => None, + }; + + if let Some(vault_operation) = updated_vault_operation { + payment_data.set_vault_operation(vault_operation); + } + }; + Ok::<_, error_stack::Report>( if let Some(payment_method_data) = payment_method_details.payment_method_data { payment_data.payment_attempt.payment_method = @@ -5824,7 +5983,6 @@ pub async fn get_payment_method_details_from_payment_token( payment_intent: &PaymentIntent, key_store: &domain::MerchantKeyStore, storage_scheme: enums::MerchantStorageScheme, - business_profile: &domain::Profile, ) -> RouterResult> { let hyperswitch_token = if let Some(token) = payment_attempt.payment_token.clone() { let redis_conn = state @@ -5899,43 +6057,31 @@ pub async fn get_payment_method_details_from_payment_token( .await } - storage::PaymentTokenData::Permanent(card_token) => retrieve_card_with_permanent_token( - state, - &card_token.token, - card_token - .payment_method_id - .as_ref() - .unwrap_or(&card_token.token), - payment_intent, - None, - key_store, - storage_scheme, - None, - None, - business_profile, - payment_attempt.connector.clone(), - ) - .await - .map(|card| Some((card, enums::PaymentMethod::Card))), + storage::PaymentTokenData::Permanent(card_token) => { + retrieve_card_with_permanent_token_for_external_authentication( + state, + &card_token.token, + payment_intent, + None, + key_store, + storage_scheme, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } - storage::PaymentTokenData::PermanentCard(card_token) => retrieve_card_with_permanent_token( - state, - &card_token.token, - card_token - .payment_method_id - .as_ref() - .unwrap_or(&card_token.token), - payment_intent, - None, - key_store, - storage_scheme, - None, - None, - business_profile, - payment_attempt.connector.clone(), - ) - .await - .map(|card| Some((card, enums::PaymentMethod::Card))), + storage::PaymentTokenData::PermanentCard(card_token) => { + retrieve_card_with_permanent_token_for_external_authentication( + state, + &card_token.token, + payment_intent, + None, + key_store, + storage_scheme, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } storage::PaymentTokenData::AuthBankDebit(auth_token) => { retrieve_payment_method_from_auth_service( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 4adb94034181..7e0fcc22ab74 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -240,6 +240,7 @@ pub trait Domain: Send + Sync { merchant_key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( BoxedOperation<'a, F, R, D>, Option, @@ -533,6 +534,7 @@ where _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRetrieveRequest, D>, Option, @@ -627,6 +629,7 @@ where _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCaptureRequest, D>, Option, @@ -732,6 +735,7 @@ where _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCancelRequest, D>, Option, @@ -816,6 +820,7 @@ where _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRejectRequest, D>, Option, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index c830b7618d07..439b639d216d 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -196,6 +196,7 @@ impl GetTracker, api::PaymentsCaptureR tax_data: None, session_id: None, service_details: None, + vault_operation: 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 c6679e481f1b..1276be6a24b7 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -207,6 +207,7 @@ impl GetTracker, api::PaymentsCancelRe tax_data: None, session_id: None, service_details: None, + vault_operation: 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 ebe49f59f649..b1cbe6c86fe8 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -256,6 +256,7 @@ impl GetTracker, api::Paymen tax_data: None, session_id: None, service_details: None, + vault_operation: 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 72c04c6a5497..18474d22f92c 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -351,6 +351,7 @@ impl GetTracker, api::PaymentsRequest> tax_data: None, session_id: None, service_details: None, + vault_operation: None, }; let customer_details = Some(CustomerDetails { @@ -408,6 +409,7 @@ impl Domain> for merchant_key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( CompleteAuthorizeOperation<'a, F>, Option, @@ -421,6 +423,7 @@ impl Domain> for customer, storage_scheme, business_profile, + should_retry_with_pan, )) .await?; Ok((op, payment_method_data, pm_id)) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index c309685a6a29..42039bdb08bb 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -823,6 +823,7 @@ impl GetTracker, api::PaymentsRequest> tax_data: None, session_id: None, service_details: request.ctp_service_details.clone(), + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -872,6 +873,7 @@ impl Domain> for key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( PaymentConfirmOperation<'a, F>, Option, @@ -885,6 +887,7 @@ impl Domain> for customer, storage_scheme, business_profile, + should_retry_with_pan, )) .await?; utils::when(payment_method_data.is_none(), || { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index b7b3420987d1..b18afd989719 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -616,6 +616,7 @@ impl GetTracker, api::PaymentsRequest> tax_data: None, session_id: None, service_details: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -767,6 +768,7 @@ impl Domain> for merchant_key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( PaymentCreateOperation<'a, F>, Option, @@ -780,6 +782,7 @@ impl Domain> for customer, storage_scheme, business_profile, + should_retry_with_pan, )) .await } diff --git a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs index 0e189583d0a2..30a94fd99a92 100644 --- a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs +++ b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs @@ -167,6 +167,7 @@ impl GetTracker, api::PaymentsPostSess tax_data: None, session_id: None, service_details: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), @@ -211,6 +212,7 @@ impl Domain, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentPostSessionTokensOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index e5321b1f9847..16031010423d 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -194,6 +194,7 @@ impl GetTracker, PaymentsCancelRequest tax_data: None, session_id: None, service_details: None, + vault_operation: 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 0e94fbe09c6f..ae4367e080cb 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -214,6 +214,7 @@ impl GetTracker, api::PaymentsSessionR tax_data: None, session_id: None, service_details: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -336,6 +337,7 @@ where _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentSessionOperation<'b, F>, Option, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 1da9a1a2264f..4b38e8efb194 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -201,6 +201,7 @@ impl GetTracker, api::PaymentsStartReq tax_data: None, session_id: None, service_details: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -308,6 +309,7 @@ where merchant_key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( PaymentSessionOperation<'a, F>, Option, @@ -328,6 +330,7 @@ where customer, storage_scheme, business_profile, + should_retry_with_pan, )) .await } else { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index dd28043ba2b8..9adeed433c45 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -95,6 +95,7 @@ impl Domain> for _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentStatusOperation<'a, F, api::PaymentsRequest>, Option, @@ -523,6 +524,7 @@ async fn get_tracker_for_sync< tax_data: None, session_id: None, service_details: None, + vault_operation: 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 0e60407aa7ca..32999b0c1afb 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -493,6 +493,7 @@ impl GetTracker, api::PaymentsRequest> tax_data: None, session_id: None, service_details: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -643,6 +644,7 @@ impl Domain> for merchant_key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( PaymentUpdateOperation<'a, F>, Option, @@ -656,6 +658,7 @@ impl Domain> for customer, storage_scheme, business_profile, + should_retry_with_pan, )) .await } 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 2c816ad39d1d..f8adf0a9e615 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -173,6 +173,7 @@ impl tax_data: None, session_id: None, service_details: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -343,6 +344,7 @@ impl _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentIncrementalAuthorizationOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/operations/tax_calculation.rs b/crates/router/src/core/payments/operations/tax_calculation.rs index 5b9c90f3add5..10da0ac16054 100644 --- a/crates/router/src/core/payments/operations/tax_calculation.rs +++ b/crates/router/src/core/payments/operations/tax_calculation.rs @@ -181,6 +181,7 @@ impl tax_data: Some(tax_data), session_id: request.session_id.clone(), service_details: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), @@ -335,6 +336,7 @@ impl Domain, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentSessionUpdateOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 7ae536a1f193..2d505ca10a49 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -98,7 +98,7 @@ where router_data = do_retry( &state.clone(), req_state.clone(), - original_connector_data, + original_connector_data.clone(), operation, customer, merchant_account, @@ -110,11 +110,13 @@ where true, frm_suggestion, business_profile, + false, ) .await?; } // Step up is not applicable so proceed with auto retries flow else { + let original_connector_data = original_connector_data.clone(); loop { // Use initial_gsm for first time alone let gsm = match initial_gsm.as_ref() { @@ -140,7 +142,22 @@ where break; } - let connector = super::get_connector_data(&mut connectors)?; + let is_network_token = matches!( + payment_data.get_payment_method_data(), + Some(domain::PaymentMethodData::NetworkToken(_)) + ); + let should_retry_with_pan = is_network_token + && initial_gsm + .as_ref() + .map(|gsm| gsm.clear_pan_possible) + .unwrap_or(false) + && business_profile.is_clear_pan_retries_enabled; + + let connector = if should_retry_with_pan { + original_connector_data.clone() + } else { + super::get_connector_data(&mut connectors)? + }; router_data = do_retry( &state.clone(), @@ -158,6 +175,7 @@ where false, frm_suggestion, business_profile, + should_retry_with_pan, ) .await?; @@ -309,6 +327,7 @@ pub async fn do_retry( is_step_up: bool, frm_suggestion: Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult> where F: Clone + Send + Sync, @@ -352,6 +371,7 @@ where frm_suggestion, business_profile, true, + should_retry_with_pan, ) .await?; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index d4c3877e5d97..199e3b23a84c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -2804,6 +2804,7 @@ impl TryFrom> for types::PaymentsAuthoriz None }; let payment_method_data = payment_data.payment_method_data.or_else(|| { + // if payment_data.mandate_id.is_some() { Some(domain::PaymentMethodData::MandatePayment) } else { @@ -2837,7 +2838,7 @@ impl TryFrom> for types::PaymentsAuthoriz let shipping_cost = payment_data.payment_intent.shipping_cost; Ok(Self { - payment_method_data: (payment_method_data.get_required_value("payment_method_data")?), + payment_method_data: (payment_method_data.get_required_value("payment_method_data")?), // setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), off_session: payment_data.mandate_id.as_ref().map(|_| true), diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index ff72af484016..758eeada8f01 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -178,6 +178,7 @@ impl ForeignTryFrom for ProfileResponse { max_auto_retries_enabled: item.max_auto_retries_enabled, is_click_to_pay_enabled: item.is_click_to_pay_enabled, authentication_product_ids: item.authentication_product_ids, + is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled, }) } } @@ -384,5 +385,6 @@ pub async fn create_profile_from_merchant_account( max_auto_retries_enabled: request.max_auto_retries_enabled.map(i16::from), is_click_to_pay_enabled: request.is_click_to_pay_enabled, authentication_product_ids, + is_clear_pan_retries_enabled: request.is_clear_pan_retries_enabled.unwrap_or_default(), })) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index bbcdfc535df8..b7db1be7bc50 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1800,6 +1800,7 @@ impl ForeignFrom for storage::GatewayStatusMapp unified_code: value.unified_code, unified_message: value.unified_message, error_category: value.error_category, + clear_pan_possible: value.clear_pan_possible, } } } @@ -1819,6 +1820,7 @@ impl ForeignFrom for gsm_api_types::GsmResponse { unified_code: value.unified_code, unified_message: value.unified_message, error_category: value.error_category, + clear_pan_possible: value.clear_pan_possible, } } } diff --git a/migrations/2024-12-10-091820_add-clear-pan-possible-to-gsm/down.sql b/migrations/2024-12-10-091820_add-clear-pan-possible-to-gsm/down.sql new file mode 100644 index 000000000000..80199b9a3348 --- /dev/null +++ b/migrations/2024-12-10-091820_add-clear-pan-possible-to-gsm/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS clear_pan_possible; \ No newline at end of file diff --git a/migrations/2024-12-10-091820_add-clear-pan-possible-to-gsm/up.sql b/migrations/2024-12-10-091820_add-clear-pan-possible-to-gsm/up.sql new file mode 100644 index 000000000000..4423df380c44 --- /dev/null +++ b/migrations/2024-12-10-091820_add-clear-pan-possible-to-gsm/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS clear_pan_possible BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/migrations/2024-12-15-171444_add-clear-pan-retries-enabled-to-profile/down.sql b/migrations/2024-12-15-171444_add-clear-pan-retries-enabled-to-profile/down.sql new file mode 100644 index 000000000000..ac61e6b3dcad --- /dev/null +++ b/migrations/2024-12-15-171444_add-clear-pan-retries-enabled-to-profile/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile DROP COLUMN IF EXISTS is_clear_pan_retries_enabled; \ No newline at end of file diff --git a/migrations/2024-12-15-171444_add-clear-pan-retries-enabled-to-profile/up.sql b/migrations/2024-12-15-171444_add-clear-pan-retries-enabled-to-profile/up.sql new file mode 100644 index 000000000000..160bd8704a43 --- /dev/null +++ b/migrations/2024-12-15-171444_add-clear-pan-retries-enabled-to-profile/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS is_clear_pan_retries_enabled BOOLEAN; \ No newline at end of file