diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 2e0d57d977..cd8b845836 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -10615,7 +10615,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -10670,6 +10671,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -10754,7 +10759,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -10811,6 +10817,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -10915,6 +10925,11 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible", + "nullable": true } } }, @@ -18738,6 +18753,11 @@ } ], "nullable": true + }, + "is_clear_pan_retries_enabled": { + "type": "boolean", + "description": "Indicates if clear pan retries is enabled or not.", + "nullable": true } }, "additionalProperties": false @@ -18771,7 +18791,8 @@ "is_tax_connector_enabled", "is_network_tokenization_enabled", "should_collect_cvv_during_payment", - "is_click_to_pay_enabled" + "is_click_to_pay_enabled", + "is_clear_pan_retries_enabled" ], "properties": { "merchant_id": { @@ -18970,6 +18991,10 @@ } ], "nullable": true + }, + "is_clear_pan_retries_enabled": { + "type": "boolean", + "description": "Indicates if clear pan retries is enabled or not." } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 060eb7038a..97352ead78 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -12830,7 +12830,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -12885,6 +12886,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -12969,7 +12974,8 @@ "message", "status", "decision", - "step_up_possible" + "step_up_possible", + "clear_pan_possible" ], "properties": { "connector": { @@ -13026,6 +13032,10 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible" } } }, @@ -13130,6 +13140,11 @@ } ], "nullable": true + }, + "clear_pan_possible": { + "type": "boolean", + "description": "indicates if retry with pan is possible", + "nullable": true } } }, @@ -23142,6 +23157,11 @@ } ], "nullable": true + }, + "is_clear_pan_retries_enabled": { + "type": "boolean", + "description": "Indicates if clear pan retries is enabled or not.", + "nullable": true } }, "additionalProperties": false @@ -23175,7 +23195,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": { @@ -23396,6 +23417,10 @@ } ], "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 d9ae29d4b3..5d31ea7099 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1960,6 +1960,9 @@ pub struct ProfileCreate { /// Card Testing Guard Configs pub card_testing_guard_config: Option, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: Option, } #[nutype::nutype( @@ -2081,6 +2084,9 @@ pub struct ProfileCreate { /// Card Testing Guard Configs pub card_testing_guard_config: Option, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v1")] @@ -2225,6 +2231,9 @@ pub struct ProfileResponse { /// Card Testing Guard Configs pub card_testing_guard_config: Option, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v2")] @@ -2352,6 +2361,9 @@ pub struct ProfileResponse { /// Card Testing Guard Configs pub card_testing_guard_config: Option, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v1")] @@ -2486,6 +2498,9 @@ pub struct ProfileUpdate { /// Card Testing Guard Configs pub card_testing_guard_config: Option, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v2")] @@ -2601,6 +2616,9 @@ pub struct ProfileUpdate { /// Card Testing Guard Configs pub card_testing_guard_config: Option, + + ///Indicates if clear pan retries is enabled or not. + pub is_clear_pan_retries_enabled: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index b95794ef70..7ffc9c6941 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/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index c5507d7800..744193bf51 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -63,6 +63,7 @@ pub struct Profile { Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v1")] @@ -111,6 +112,7 @@ pub struct ProfileNew { Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v1")] @@ -157,6 +159,7 @@ pub struct ProfileUpdateInternal { Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v1")] @@ -201,6 +204,7 @@ impl ProfileUpdateInternal { authentication_product_ids, card_testing_guard_config, card_testing_secret_key, + is_clear_pan_retries_enabled, } = self; Profile { profile_id: source.profile_id, @@ -269,6 +273,8 @@ impl ProfileUpdateInternal { card_testing_guard_config: card_testing_guard_config .or(source.card_testing_guard_config), card_testing_secret_key, + is_clear_pan_retries_enabled: is_clear_pan_retries_enabled + .unwrap_or(source.is_clear_pan_retries_enabled), } } } @@ -321,6 +327,7 @@ pub struct Profile { Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, + pub is_clear_pan_retries_enabled: bool, pub routing_algorithm_id: Option, pub order_fulfillment_time: Option, pub order_fulfillment_time_origin: Option, @@ -394,6 +401,7 @@ pub struct ProfileNew { pub three_ds_decision_manager_config: Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v2")] @@ -442,6 +450,7 @@ pub struct ProfileUpdateInternal { pub three_ds_decision_manager_config: Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v2")] @@ -488,6 +497,7 @@ impl ProfileUpdateInternal { three_ds_decision_manager_config, card_testing_guard_config, card_testing_secret_key, + is_clear_pan_retries_enabled, } = self; Profile { id: source.id, @@ -562,6 +572,8 @@ impl ProfileUpdateInternal { card_testing_guard_config: card_testing_guard_config .or(source.card_testing_guard_config), card_testing_secret_key: card_testing_secret_key.or(source.card_testing_secret_key), + is_clear_pan_retries_enabled: is_clear_pan_retries_enabled + .unwrap_or(source.is_clear_pan_retries_enabled), } } } diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index aba8d75a6e..46583a5b22 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 1de8483a3f..9135baeb77 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -219,6 +219,7 @@ diesel::table! { authentication_product_ids -> Nullable, card_testing_guard_config -> Nullable, card_testing_secret_key -> Nullable, + is_clear_pan_retries_enabled -> Bool, } } @@ -565,6 +566,7 @@ diesel::table! { unified_message -> Nullable, #[max_length = 64] error_category -> Nullable, + clear_pan_possible -> Bool, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 6018d0a699..af1b2c65f4 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -215,6 +215,7 @@ diesel::table! { authentication_product_ids -> Nullable, card_testing_guard_config -> Nullable, card_testing_secret_key -> Nullable, + is_clear_pan_retries_enabled -> Bool, #[max_length = 64] routing_algorithm_id -> Nullable, order_fulfillment_time -> Nullable, @@ -576,6 +577,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 07fd0a9fd9..d4bb6ed1fc 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -64,6 +64,7 @@ pub struct Profile { Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v1")] @@ -110,6 +111,7 @@ pub struct ProfileSetter { Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v1")] @@ -162,6 +164,7 @@ impl From for Profile { authentication_product_ids: value.authentication_product_ids, card_testing_guard_config: value.card_testing_guard_config, card_testing_secret_key: value.card_testing_secret_key, + is_clear_pan_retries_enabled: value.is_clear_pan_retries_enabled, } } } @@ -216,6 +219,7 @@ pub struct ProfileGeneralUpdate { Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, + pub is_clear_pan_retries_enabled: Option, } #[cfg(feature = "v1")] @@ -285,6 +289,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids, card_testing_guard_config, card_testing_secret_key, + is_clear_pan_retries_enabled, } = *update; Self { @@ -327,6 +332,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids, card_testing_guard_config, card_testing_secret_key: card_testing_secret_key.map(Encryption::from), + is_clear_pan_retries_enabled, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -371,6 +377,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -413,6 +420,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -455,6 +463,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -497,6 +506,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -539,6 +549,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::CardTestingSecretKeyUpdate { card_testing_secret_key, @@ -581,6 +592,7 @@ impl From for ProfileUpdateInternal { authentication_product_ids: None, card_testing_guard_config: None, card_testing_secret_key: card_testing_secret_key.map(Encryption::from), + is_clear_pan_retries_enabled: None, }, } } @@ -642,6 +654,7 @@ impl super::behaviour::Conversion for Profile { authentication_product_ids: self.authentication_product_ids, card_testing_guard_config: self.card_testing_guard_config, card_testing_secret_key: self.card_testing_secret_key.map(|name| name.into()), + is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, }) } @@ -728,6 +741,7 @@ impl super::behaviour::Conversion for Profile { .and_then(|val| val.try_into_optionaloperation()) }) .await?, + is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled, }) } .await @@ -784,6 +798,7 @@ impl super::behaviour::Conversion for Profile { authentication_product_ids: self.authentication_product_ids, card_testing_guard_config: self.card_testing_guard_config, card_testing_secret_key: self.card_testing_secret_key.map(Encryption::from), + is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, }) } } @@ -834,6 +849,7 @@ pub struct Profile { pub three_ds_decision_manager_config: Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v2")] @@ -880,6 +896,7 @@ pub struct ProfileSetter { pub three_ds_decision_manager_config: Option, pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, + pub is_clear_pan_retries_enabled: bool, } #[cfg(feature = "v2")] @@ -932,6 +949,7 @@ impl From for Profile { three_ds_decision_manager_config: value.three_ds_decision_manager_config, card_testing_guard_config: value.card_testing_guard_config, card_testing_secret_key: value.card_testing_secret_key, + is_clear_pan_retries_enabled: value.is_clear_pan_retries_enabled, } } } @@ -1100,6 +1118,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config, card_testing_guard_config, card_testing_secret_key: card_testing_secret_key.map(Encryption::from), + is_clear_pan_retries_enabled: None, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -1146,6 +1165,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -1190,6 +1210,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -1234,6 +1255,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::DefaultRoutingFallbackUpdate { default_fallback_routing, @@ -1278,6 +1300,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -1322,6 +1345,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::CollectCvvDuringPaymentUpdate { should_collect_cvv_during_payment, @@ -1366,6 +1390,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::DecisionManagerRecordUpdate { three_ds_decision_manager_config, @@ -1410,6 +1435,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: Some(three_ds_decision_manager_config), card_testing_guard_config: None, card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, }, ProfileUpdate::CardTestingSecretKeyUpdate { card_testing_secret_key, @@ -1454,6 +1480,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: card_testing_secret_key.map(Encryption::from), + is_clear_pan_retries_enabled: None, }, } } @@ -1519,6 +1546,7 @@ impl super::behaviour::Conversion for Profile { three_ds_decision_manager_config: self.three_ds_decision_manager_config, card_testing_guard_config: self.card_testing_guard_config, card_testing_secret_key: self.card_testing_secret_key.map(|name| name.into()), + is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, }) } @@ -1605,6 +1633,7 @@ impl super::behaviour::Conversion for Profile { .and_then(|val| val.try_into_optionaloperation()) }) .await?, + is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled, }) } .await @@ -1665,6 +1694,7 @@ impl super::behaviour::Conversion for Profile { three_ds_decision_manager_config: self.three_ds_decision_manager_config, card_testing_guard_config: self.card_testing_guard_config, card_testing_secret_key: self.card_testing_secret_key.map(Encryption::from), + is_clear_pan_retries_enabled: Some(self.is_clear_pan_retries_enabled), }) } } diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 2f8375b47c..78b2144f3e 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -71,6 +71,10 @@ impl PaymentMethodData { Self::CardToken(_) | Self::MandatePayment => None, } } + + pub fn is_network_token_payment_method_data(&self) -> bool { + matches!(self, Self::NetworkToken(_)) + } } #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Default)] diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 606a8af51c..d7816915ba 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -35,13 +35,13 @@ use diesel_models::{ }; use self::payment_attempt::PaymentAttempt; -#[cfg(feature = "v1")] -use crate::RemoteStorageObject; #[cfg(feature = "v2")] use crate::{ address::Address, business_profile, errors, merchant_account, payment_address, payment_method_data, ApiModelToDieselModelConvertor, }; +#[cfg(feature = "v1")] +use crate::{payment_method_data, RemoteStorageObject}; #[cfg(feature = "v1")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToEncryption)] @@ -633,3 +633,80 @@ where &self.payment_intent.id } } + +#[derive(Clone, serde::Serialize, Debug)] +pub enum VaultOperation { + ExistingVaultData(VaultData), +} + +impl VaultOperation { + pub fn get_updated_vault_data( + existing_vault_data: Option<&Self>, + payment_method_data: &payment_method_data::PaymentMethodData, + ) -> Option { + match (existing_vault_data, payment_method_data) { + (None, payment_method_data::PaymentMethodData::Card(card)) => { + Some(Self::ExistingVaultData(VaultData::Card(card.clone()))) + } + (None, payment_method_data::PaymentMethodData::NetworkToken(nt_data)) => Some( + Self::ExistingVaultData(VaultData::NetworkToken(nt_data.clone())), + ), + (Some(Self::ExistingVaultData(vault_data)), payment_method_data) => { + match (vault_data, payment_method_data) { + ( + VaultData::Card(card), + payment_method_data::PaymentMethodData::NetworkToken(nt_data), + ) => Some(Self::ExistingVaultData(VaultData::CardAndNetworkToken( + Box::new(CardAndNetworkTokenData { + card_data: card.clone(), + network_token_data: nt_data.clone(), + }), + ))), + ( + VaultData::NetworkToken(nt_data), + payment_method_data::PaymentMethodData::Card(card), + ) => Some(Self::ExistingVaultData(VaultData::CardAndNetworkToken( + Box::new(CardAndNetworkTokenData { + card_data: card.clone(), + network_token_data: nt_data.clone(), + }), + ))), + _ => Some(Self::ExistingVaultData(vault_data.clone())), + } + } + //payment_method_data is not card or network token + _ => None, + } + } +} + +#[derive(Clone, serde::Serialize, Debug)] +pub enum VaultData { + Card(payment_method_data::Card), + NetworkToken(payment_method_data::NetworkTokenData), + CardAndNetworkToken(Box), +} + +#[derive(Default, Clone, serde::Serialize, Debug)] +pub struct CardAndNetworkTokenData { + pub card_data: payment_method_data::Card, + pub network_token_data: payment_method_data::NetworkTokenData, +} + +impl VaultData { + pub fn get_card_vault_data(&self) -> Option { + match self { + Self::Card(card_data) => Some(card_data.clone()), + Self::NetworkToken(_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::Card(_card_data) => None, + Self::NetworkToken(network_token_data) => Some(network_token_data.clone()), + Self::CardAndNetworkToken(vault_data) => Some(vault_data.network_token_data.clone()), + } + } +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 38b1ddb0f8..157b857052 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3786,6 +3786,7 @@ impl ProfileCreateBridge for api::ProfileCreate { .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error while generating card testing secret key")?, + is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled.unwrap_or_default(), })) } @@ -3941,6 +3942,7 @@ impl ProfileCreateBridge for api::ProfileCreate { .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error while generating card testing secret key")?, + is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled.unwrap_or_default(), })) } } @@ -4227,6 +4229,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { .card_testing_guard_config .map(ForeignInto::foreign_into), card_testing_secret_key, + 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 c7daee111a..6c544791a1 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 9f0fd70522..452364b654 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::{ @@ -47,7 +47,9 @@ use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData}; use hyperswitch_domain_models::mandates::CommonMandateReference; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use hyperswitch_domain_models::payment_method_data; -use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use hyperswitch_domain_models::payments::{ + payment_attempt::PaymentAttempt, PaymentIntent, VaultData, +}; use masking::{PeekInterface, Secret}; use router_env::{instrument, tracing}; use time::Duration; @@ -541,6 +543,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<&VaultData>, ) -> RouterResult { let token = match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { @@ -584,7 +588,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 @@ -596,9 +600,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)))? @@ -619,7 +628,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 @@ -631,9 +640,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 7ea847ce65..f29bcb98ee 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -51,7 +51,7 @@ use hyperswitch_domain_models::router_response_types::RedirectForm; pub use hyperswitch_domain_models::{ mandates::{CustomerAcceptance, MandateData}, payment_address::PaymentAddress, - payments::HeaderPayload, + payments::{self as domain_payments, HeaderPayload}, router_data::{PaymentMethodToken, RouterData}, router_request_types::CustomerDetails, }; @@ -214,6 +214,7 @@ where None, profile, false, + false, //should_retry_with_pan is set to false in case of PreDetermined ConnectorCallType ) .await?; @@ -519,6 +520,7 @@ where None, &business_profile, false, + false, ) .await?; @@ -638,6 +640,7 @@ where None, &business_profile, false, + false, ) .await?; @@ -659,7 +662,7 @@ where req_state.clone(), &mut payment_data, connectors, - connector_data.clone(), + &connector_data, router_data, &merchant_account, &key_store, @@ -2738,6 +2741,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, @@ -2790,6 +2794,7 @@ where key_store, customer, business_profile, + should_retry_with_pan, ) .await?; *payment_data = pd; @@ -2990,6 +2995,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, @@ -4705,6 +4711,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, @@ -4776,6 +4783,7 @@ where merchant_key_store, customer, business_profile, + should_retry_with_pan, ) .await?; payment_data.set_payment_method_data(payment_method_data); @@ -4795,6 +4803,7 @@ where merchant_key_store, customer, business_profile, + should_retry_with_pan, ) .await?; @@ -4883,6 +4892,7 @@ where merchant_key_store, customer, business_profile, + false, ) .await?; payment_data.set_payment_method_data(payment_method_data); @@ -4950,6 +4960,7 @@ where pub service_details: Option, pub card_testing_guard_data: Option, + pub vault_operation: Option, } #[derive(Clone, serde::Serialize, Debug)] @@ -7190,7 +7201,6 @@ pub async fn payment_external_authentication( &payment_intent, &key_store, storage_scheme, - &business_profile, ) .await? .ok_or(errors::ApiErrorResponse::InternalServerError) @@ -7603,6 +7613,9 @@ pub trait OperationSessionGetters { fn get_force_sync(&self) -> Option; fn get_capture_method(&self) -> Option; + #[cfg(feature = "v1")] + fn get_vault_operation(&self) -> Option<&domain_payments::VaultOperation>; + #[cfg(feature = "v2")] fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt>; } @@ -7647,6 +7660,9 @@ pub trait OperationSessionSetters { straight_through_algorithm: serde_json::Value, ); fn set_connector_in_payment_attempt(&mut self, connector: Option); + + #[cfg(feature = "v1")] + fn set_vault_operation(&mut self, vault_operation: domain_payments::VaultOperation); } #[cfg(feature = "v1")] @@ -7779,6 +7795,11 @@ impl OperationSessionGetters for PaymentData { self.payment_attempt.capture_method } + #[cfg(feature = "v1")] + fn get_vault_operation(&self) -> Option<&domain_payments::VaultOperation> { + self.vault_operation.as_ref() + } + // #[cfg(feature = "v2")] // fn get_capture_method(&self) -> Option { // Some(self.payment_intent.capture_method) @@ -7895,6 +7916,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: domain_payments::VaultOperation) { + 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 21d84d3cba..05b4d592c2 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -32,8 +32,8 @@ use hyperswitch_domain_models::{ mandates::MandateData, payment_method_data::{GetPaymentMethodType, PazeWalletData}, payments::{ - payment_attempt::PaymentAttempt, payment_intent::PaymentIntentFetchConstraints, - PaymentIntent, + self as domain_payments, payment_attempt::PaymentAttempt, + payment_intent::PaymentIntentFetchConstraints, PaymentIntent, }, router_data::KlarnaSdkResponse, }; @@ -1988,12 +1988,96 @@ pub async fn retrieve_card_with_permanent_token( todo!() } +pub enum VaultFetchAction { + FetchCardDetailsFromLocker, + FetchCardDetailsForNetworkTransactionIdFlowFromLocker, + 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: &HashSet, + should_retry_with_pan: bool, + network_token_requestor_ref_id: Option, +) -> VaultFetchAction { + let standard_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(standard_flow) +} + +pub fn determine_standard_vault_action( + is_network_tokenization_enabled: bool, + mandate_id: Option, + connector: Option, + network_tokenization_supported_connectors: &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::FetchCardDetailsForNetworkTransactionIdFlowFromLocker + } 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::FetchCardDetailsForNetworkTransactionIdFlowFromLocker + } + Some(api_models::payments::MandateReferenceId::ConnectorMandateId(_)) | None => { + VaultFetchAction::NoFetchAction + } + }, + None => { + //saved card flow + let is_network_token_supported_connector = connector + .map(|conn| network_tokenization_supported_connectors.contains(&conn)) + .unwrap_or(false); + + match ( + is_network_token_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, @@ -2002,9 +2086,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<&domain_payments::VaultData>, ) -> RouterResult { let customer_id = payment_intent .customer_id @@ -2014,121 +2100,38 @@ 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(), - }; - - Ok( - domain::PaymentMethodData::CardDetailsForNetworkTransactionId( - card_details_for_network_transaction_id, - ), - ) - } else { - 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 { - 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"); + 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 = vault_data + .and_then(|vault_data| vault_data.get_card_vault_data()) + .map(Ok) + .async_unwrap_or_else(|| async { fetch_card_details_from_locker( state, customer_id, @@ -2137,115 +2140,136 @@ pub async fn retrieve_card_with_permanent_token( card_token_data, ) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch card information from the permanent locker") + }) + .await?; + + Ok(domain::PaymentMethodData::Card(card)) + } + VaultFetchAction::FetchCardDetailsForNetworkTransactionIdFlowFromLocker => { + fetch_card_details_for_network_transaction_flow_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + locker_id, + ) + .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(|vault_data| vault_data.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( - 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:?}"); }) - .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?, + )) } } } + 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(|vault_data| vault_data.get_network_token_data()) + .map(Ok) + .async_unwrap_or_else(|| async { + fetch_network_token_details_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + network_token_locker_id, + nt_data, + ) + .await + }) + .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")?, + )) +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] 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) @@ -2290,7 +2314,106 @@ 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()) +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] +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, + network_transaction_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 = network_transaction_data + .token_exp_month + .zip(network_transaction_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 card_network = token_data + .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 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, + card_type: None, + card_issuing_country: None, + bank_code: None, + eci: None, + }; + Ok(network_token_data) +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] +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"))] @@ -2424,6 +2547,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, @@ -2432,11 +2556,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 @@ -2484,6 +2612,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 existing_vault_data = payment_data.get_vault_operation(); + + let vault_data = existing_vault_data.map(|data| match data { + domain_payments::VaultOperation::ExistingVaultData(vault_data) => vault_data, + }); + let pm_data = Box::pin(payment_methods::retrieve_payment_method_with_token( state, merchant_key_store, @@ -2496,11 +2630,25 @@ 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, + vault_data, )) .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 = + domain_payments::VaultOperation::get_updated_vault_data( + existing_vault_data, + payment_method_data, + ); + + 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 = @@ -6471,7 +6619,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 @@ -6546,43 +6693,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 a1ebaa0415..fde773de8a 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -261,6 +261,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, @@ -557,6 +558,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, @@ -651,6 +653,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, @@ -756,6 +759,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, @@ -840,6 +844,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 393d08a68d..3ab64a1361 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -201,6 +201,7 @@ impl GetTracker, api::PaymentsCaptureR session_id: None, service_details: None, card_testing_guard_data: 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 d713e7aa56..f5ce637a5b 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -212,6 +212,7 @@ impl GetTracker, api::PaymentsCancelRe session_id: None, service_details: None, card_testing_guard_data: 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 2a58adb822..a6c9594eb8 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -261,6 +261,7 @@ impl GetTracker, api::Paymen session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture_v2.rs b/crates/router/src/core/payments/operations/payment_capture_v2.rs index 93b7ffa6c1..f69dd8de62 100644 --- a/crates/router/src/core/payments/operations/payment_capture_v2.rs +++ b/crates/router/src/core/payments/operations/payment_capture_v2.rs @@ -247,6 +247,7 @@ impl Domain> f _key_store: &domain::MerchantKeyStore, _customer: &Option, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( BoxedConfirmOperation<'a, F>, Option, 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 fab2a9144c..2dc118a60f 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -355,6 +355,7 @@ impl GetTracker, api::PaymentsRequest> session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let customer_details = Some(CustomerDetails { @@ -412,6 +413,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, @@ -425,6 +427,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 852e51f4a8..556954155e 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -827,6 +827,7 @@ impl GetTracker, api::PaymentsRequest> session_id: None, service_details: request.ctp_service_details.clone(), card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -909,6 +910,7 @@ impl Domain> for key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( PaymentConfirmOperation<'a, F>, Option, @@ -922,6 +924,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_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 40b1918323..a5b0e6eb65 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -321,6 +321,7 @@ impl Domain, business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( BoxedConfirmOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 06036c4bc5..7211b88a10 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -626,6 +626,7 @@ impl GetTracker, api::PaymentsRequest> session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -777,6 +778,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, @@ -790,6 +792,7 @@ impl Domain> for customer, storage_scheme, business_profile, + should_retry_with_pan, )) .await } diff --git a/crates/router/src/core/payments/operations/payment_create_intent.rs b/crates/router/src/core/payments/operations/payment_create_intent.rs index 1cd0010bdf..755c4e153c 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -258,6 +258,7 @@ impl Domain, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentsCreateIntentOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/operations/payment_get.rs b/crates/router/src/core/payments/operations/payment_get.rs index fdf5350bed..f15990d67b 100644 --- a/crates/router/src/core/payments/operations/payment_get.rs +++ b/crates/router/src/core/payments/operations/payment_get.rs @@ -251,6 +251,7 @@ impl Domain, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( BoxedConfirmOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/operations/payment_get_intent.rs b/crates/router/src/core/payments/operations/payment_get_intent.rs index 0041209eb0..05f939ee31 100644 --- a/crates/router/src/core/payments/operations/payment_get_intent.rs +++ b/crates/router/src/core/payments/operations/payment_get_intent.rs @@ -190,6 +190,7 @@ impl Domain, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentsGetIntentOperation<'a, F>, Option, 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 0a3efc92cc..45a659c85b 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 @@ -170,6 +170,7 @@ impl GetTracker, api::PaymentsPostSess session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), @@ -214,6 +215,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 91317dbde6..1d919fc45b 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -199,6 +199,7 @@ impl GetTracker, PaymentsCancelRequest session_id: None, service_details: None, card_testing_guard_data: 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 a86ca0be4e..5833ec3e87 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -217,6 +217,7 @@ impl GetTracker, api::PaymentsSessionR session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -339,6 +340,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_session_intent.rs b/crates/router/src/core/payments/operations/payment_session_intent.rs index 9bca88c6d0..b2ab23223d 100644 --- a/crates/router/src/core/payments/operations/payment_session_intent.rs +++ b/crates/router/src/core/payments/operations/payment_session_intent.rs @@ -197,6 +197,7 @@ impl Domain, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentsCreateIntentOperation<'a, 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 ebae54e122..8023f9ed95 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -204,6 +204,7 @@ impl GetTracker, api::PaymentsStartReq session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -311,6 +312,7 @@ where merchant_key_store: &domain::MerchantKeyStore, customer: &Option, business_profile: &domain::Profile, + should_retry_with_pan: bool, ) -> RouterResult<( PaymentSessionOperation<'a, F>, Option, @@ -331,6 +333,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 e05eed4141..bcfe90367d 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, @@ -535,6 +536,7 @@ async fn get_tracker_for_sync< session_id: None, service_details: None, card_testing_guard_data: 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 89a94552bc..e7cc8a41b8 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -496,6 +496,7 @@ impl GetTracker, api::PaymentsRequest> session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -646,6 +647,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, @@ -659,6 +661,7 @@ impl Domain> for customer, storage_scheme, business_profile, + should_retry_with_pan, )) .await } diff --git a/crates/router/src/core/payments/operations/payment_update_intent.rs b/crates/router/src/core/payments/operations/payment_update_intent.rs index af6b7d5783..7662c07eee 100644 --- a/crates/router/src/core/payments/operations/payment_update_intent.rs +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -419,6 +419,7 @@ impl Domain, _business_profile: &domain::Profile, + _should_retry_with_pan: bool, ) -> RouterResult<( PaymentsUpdateIntentOperation<'a, F>, Option, 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 8ef656599a..cdd823483e 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -178,6 +178,7 @@ impl session_id: None, service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { @@ -348,6 +349,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 0d31c4d5de..aac91e20c1 100644 --- a/crates/router/src/core/payments/operations/tax_calculation.rs +++ b/crates/router/src/core/payments/operations/tax_calculation.rs @@ -184,6 +184,7 @@ impl session_id: request.session_id.clone(), service_details: None, card_testing_guard_data: None, + vault_operation: None, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), @@ -338,6 +339,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 d113cfd55d..5fdf14dbf5 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -35,7 +35,7 @@ pub async fn do_gsm_actions( req_state: ReqState, payment_data: &mut D, mut connectors: IntoIter, - original_connector_data: api::ConnectorData, + original_connector_data: &api::ConnectorData, mut router_data: types::RouterData, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, @@ -110,6 +110,7 @@ where true, frm_suggestion, business_profile, + false, //should_retry_with_pan is not applicable for step-up ) .await?; } @@ -140,12 +141,29 @@ where break; } - let connector = super::get_connector_data(&mut connectors)?; + let is_network_token = payment_data + .get_payment_method_data() + .map(|pmd| pmd.is_network_token_payment_method_data()) + .unwrap_or(false); + + 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 { + // If should_retry_with_pan is true, it indicates that we are retrying with PAN using the same connector. + original_connector_data.clone() + } else { + super::get_connector_data(&mut connectors)? + }; router_data = do_retry( &state.clone(), req_state.clone(), - connector, + &connector, operation, customer, merchant_account, @@ -158,6 +176,7 @@ where false, frm_suggestion, business_profile, + should_retry_with_pan, ) .await?; @@ -297,7 +316,7 @@ fn get_flow_name() -> RouterResult { pub async fn do_retry( state: &routes::SessionState, req_state: ReqState, - connector: api::ConnectorData, + connector: &api::ConnectorData, operation: &operations::BoxedOperation<'_, F, ApiRequest, D>, customer: &Option, merchant_account: &domain::MerchantAccount, @@ -309,6 +328,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, @@ -341,7 +361,7 @@ where req_state, merchant_account, key_store, - connector, + connector.clone(), operation, payment_data, customer, @@ -352,6 +372,7 @@ where frm_suggestion, business_profile, true, + should_retry_with_pan, ) .await?; diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 2852766cdd..67e8990e11 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -187,6 +187,7 @@ impl ForeignTryFrom for ProfileResponse { card_testing_guard_config: item .card_testing_guard_config .map(ForeignInto::foreign_into), + is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled, }) } } @@ -261,6 +262,7 @@ impl ForeignTryFrom for ProfileResponse { card_testing_guard_config: item .card_testing_guard_config .map(ForeignInto::foreign_into), + is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled, }) } } @@ -435,5 +437,6 @@ pub async fn create_profile_from_merchant_account( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error while generating card testing secret key")?, + 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 8938461eec..1035aed2f9 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1838,6 +1838,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, } } } @@ -1857,6 +1858,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 0000000000..80199b9a33 --- /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 0000000000..4423df380c --- /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/2025-02-27-171444_add-clear-pan-retries-enabled-to-profile/down.sql b/migrations/2025-02-27-171444_add-clear-pan-retries-enabled-to-profile/down.sql new file mode 100644 index 0000000000..ac61e6b3dc --- /dev/null +++ b/migrations/2025-02-27-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/2025-02-27-171444_add-clear-pan-retries-enabled-to-profile/up.sql b/migrations/2025-02-27-171444_add-clear-pan-retries-enabled-to-profile/up.sql new file mode 100644 index 0000000000..9a87772929 --- /dev/null +++ b/migrations/2025-02-27-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 NOT NULL DEFAULT FALSE; \ No newline at end of file