From 17f5577fbdbdf332cae9875fa58a045a7231e891 Mon Sep 17 00:00:00 2001 From: Sagnik Mitra Date: Wed, 25 Dec 2024 11:42:44 +0530 Subject: [PATCH] post payment processing --- crates/api_models/src/admin.rs | 24 +- crates/diesel_models/src/business_profile.rs | 18 +- crates/diesel_models/src/schema.rs | 2 +- crates/diesel_models/src/schema_v2.rs | 1 + .../src/business_profile.rs | 56 +-- crates/router/src/core/admin.rs | 6 +- crates/router/src/core/payments.rs | 299 +++++++-------- .../payments/operations/payment_response.rs | 11 +- .../router/src/core/payments/tokenization.rs | 343 ++++++++++++++---- crates/router/src/types/api/admin.rs | 5 +- .../down.sql | 2 - .../up.sql | 2 - .../down.sql | 2 + .../up.sql | 2 + 14 files changed, 504 insertions(+), 269 deletions(-) delete mode 100644 migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/down.sql delete mode 100644 migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/up.sql create mode 100644 migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/down.sql create mode 100644 migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/up.sql diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index cd152f9bb573..7c9df019fb1f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1868,9 +1868,9 @@ pub struct ProfileCreate { pub authentication_product_ids: Option, - /// Indicates if network tokenization before first payment is enabled or not + /// Indicates if pre network tokenization is enabled or not #[serde(default)] - pub is_tokenize_before_payment_enabled: bool, + pub is_pre_network_tokenization_enabled: bool, } #[nutype::nutype( @@ -1990,9 +1990,9 @@ pub struct ProfileCreate { pub authentication_product_ids: Option, - /// Indicates if network tokenization before first payment is enabled or not + /// Indicates if pre network tokenization is enabled or not #[serde(default)] - pub is_tokenize_before_payment_enabled: bool, + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -2131,8 +2131,8 @@ pub struct ProfileResponse { pub authentication_product_ids: Option, - /// Indicates if network tokenization before first payment is enabled or not - pub is_tokenize_before_payment_enabled: bool, + /// Indicates if pre network tokenization is enabled or not + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v2")] @@ -2258,8 +2258,8 @@ pub struct ProfileResponse { pub authentication_product_ids: Option, - /// Indicates if network tokenization before first payment is enabled or not - pub is_tokenize_before_payment_enabled: bool, + /// Indicates if pre network tokenization is enabled or not + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -2392,9 +2392,9 @@ pub struct ProfileUpdate { pub authentication_product_ids: Option, - /// Indicates if network tokenization before first payment is enabled or not + /// Indicates if pre network tokenization is enabled or not #[schema(default = false, example = false)] - pub is_tokenize_before_payment_enabled: Option, + pub is_pre_network_tokenization_enabled: Option, } #[cfg(feature = "v2")] @@ -2508,9 +2508,9 @@ pub struct ProfileUpdate { pub authentication_product_ids: Option, - /// Indicates if network tokenization before first payment is enabled or not + /// Indicates if pre network tokenization is enabled or not #[schema(default = false, example = false)] - pub is_tokenize_before_payment_enabled: Option, + pub is_pre_network_tokenization_enabled: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 1d5ad52384e0..da78186715fa 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -60,7 +60,7 @@ pub struct Profile { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: bool, + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -107,7 +107,7 @@ pub struct ProfileNew { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: Option, + pub is_pre_network_tokenization_enabled: Option, } #[cfg(feature = "v1")] @@ -151,7 +151,7 @@ pub struct ProfileUpdateInternal { pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: Option, + pub is_pre_network_tokenization_enabled: Option, } #[cfg(feature = "v1")] @@ -193,7 +193,7 @@ impl ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, - is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled, } = self; Profile { profile_id: source.profile_id, @@ -257,8 +257,8 @@ impl ProfileUpdateInternal { .unwrap_or(source.is_click_to_pay_enabled), authentication_product_ids: authentication_product_ids .or(source.authentication_product_ids), - is_tokenize_before_payment_enabled: is_tokenize_before_payment_enabled - .unwrap_or(source.is_tokenize_before_payment_enabled), + is_pre_network_tokenization_enabled: is_pre_network_tokenization_enabled + .unwrap_or(source.is_pre_network_tokenization_enabled), } } } @@ -316,6 +316,7 @@ pub struct Profile { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub is_pre_network_tokenization_enabled: bool, } impl Profile { @@ -377,6 +378,7 @@ pub struct ProfileNew { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub is_pre_network_tokenization_enabled: Option, } #[cfg(feature = "v2")] @@ -422,6 +424,7 @@ pub struct ProfileUpdateInternal { pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, + pub is_pre_network_tokenization_enabled: Option, } #[cfg(feature = "v2")] @@ -465,6 +468,7 @@ impl ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, + is_pre_network_tokenization_enabled, } = self; Profile { id: source.id, @@ -533,6 +537,8 @@ impl ProfileUpdateInternal { .unwrap_or(source.is_click_to_pay_enabled), authentication_product_ids: authentication_product_ids .or(source.authentication_product_ids), + is_pre_network_tokenization_enabled: is_pre_network_tokenization_enabled + .unwrap_or(source.is_pre_network_tokenization_enabled) } } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index d01bae3fe462..aca30da620be 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -216,7 +216,7 @@ diesel::table! { max_auto_retries_enabled -> Nullable, is_click_to_pay_enabled -> Bool, authentication_product_ids -> Nullable, - is_tokenize_before_payment_enabled -> Bool, + is_pre_network_tokenization_enabled -> Bool, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index b5361f4c95cd..4b6721b4d819 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -224,6 +224,7 @@ diesel::table! { max_auto_retries_enabled -> Nullable, is_click_to_pay_enabled -> Bool, authentication_product_ids -> Nullable, + is_pre_network_tokenization_enabled -> Bool, } } diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index 8b34206c3fbc..ef09ecf84001 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -61,7 +61,7 @@ pub struct Profile { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: bool, + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -105,7 +105,7 @@ pub struct ProfileSetter { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: bool, + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -155,7 +155,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_tokenize_before_payment_enabled: value.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: value.is_pre_network_tokenization_enabled, } } } @@ -208,7 +208,7 @@ pub struct ProfileGeneralUpdate { pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: Option, + pub is_pre_network_tokenization_enabled: Option, } #[cfg(feature = "v1")] @@ -273,7 +273,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, - is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled, } = *update; Self { @@ -313,7 +313,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, - is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -355,7 +355,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -395,7 +395,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -435,7 +435,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -475,7 +475,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -515,7 +515,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, } } @@ -574,7 +574,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_tokenize_before_payment_enabled: self.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: self.is_pre_network_tokenization_enabled, }) } @@ -645,7 +645,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_tokenize_before_payment_enabled: item.is_network_tokenization_enabled, + is_pre_network_tokenization_enabled: item.is_network_tokenization_enabled, }) } .await @@ -700,7 +700,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_tokenize_before_payment_enabled: Some(self.is_tokenize_before_payment_enabled), + is_pre_network_tokenization_enabled: Some(self.is_pre_network_tokenization_enabled), }) } } @@ -748,7 +748,7 @@ pub struct Profile { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: bool, + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v2")] @@ -792,7 +792,7 @@ pub struct ProfileSetter { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, - pub is_tokenize_before_payment_enabled: bool, + pub is_pre_network_tokenization_enabled: bool, } #[cfg(feature = "v2")] @@ -842,7 +842,7 @@ impl From for Profile { is_network_tokenization_enabled: value.is_network_tokenization_enabled, is_click_to_pay_enabled: value.is_click_to_pay_enabled, authentication_product_ids: value.authentication_product_ids, - is_tokenize_before_payment_enabled: value.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: value.is_pre_network_tokenization_enabled, } } } @@ -896,6 +896,7 @@ pub struct ProfileGeneralUpdate { pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, + pub is_pre_network_tokenization_enabled: Option, } #[cfg(feature = "v2")] @@ -956,6 +957,7 @@ impl From for ProfileUpdateInternal { is_network_tokenization_enabled, is_click_to_pay_enabled, authentication_product_ids, + is_pre_network_tokenization_enabled, } = *update; Self { profile_name, @@ -996,7 +998,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -1040,7 +1042,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -1082,7 +1084,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -1124,7 +1126,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::DefaultRoutingFallbackUpdate { default_fallback_routing, @@ -1166,7 +1168,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -1208,7 +1210,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, ProfileUpdate::CollectCvvDuringPaymentUpdate { should_collect_cvv_during_payment, @@ -1250,7 +1252,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, - is_tokenize_before_payment_enabled: None, + is_pre_network_tokenization_enabled: None, }, } } @@ -1312,7 +1314,7 @@ impl super::behaviour::Conversion for Profile { max_auto_retries_enabled: None, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, - is_tokenize_before_payment_enabled: self.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: self.is_pre_network_tokenization_enabled, }) } @@ -1383,7 +1385,7 @@ impl super::behaviour::Conversion for Profile { is_network_tokenization_enabled: item.is_network_tokenization_enabled, is_click_to_pay_enabled: item.is_click_to_pay_enabled, authentication_product_ids: item.authentication_product_ids, - is_tokenize_before_payment_enabled: item.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: item.is_pre_network_tokenization_enabled, }) } .await @@ -1441,7 +1443,7 @@ impl super::behaviour::Conversion for Profile { max_auto_retries_enabled: None, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, - is_tokenize_before_payment_enabled: self.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: Some(self.is_pre_network_tokenization_enabled), }) } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 447a2fe3f90c..cda8f60fb0ee 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3689,7 +3689,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: self.authentication_product_ids, - is_tokenize_before_payment_enabled: self.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: self.is_pre_network_tokenization_enabled, })) } @@ -3799,6 +3799,7 @@ impl ProfileCreateBridge for api::ProfileCreate { is_network_tokenization_enabled: self.is_network_tokenization_enabled, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + is_pre_network_tokenization_enabled: self.is_pre_network_tokenization_enabled, })) } } @@ -4050,7 +4051,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: self.authentication_product_ids, - is_tokenize_before_payment_enabled: self.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: self.is_pre_network_tokenization_enabled, }, ))) } @@ -4149,6 +4150,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { is_network_tokenization_enabled: self.is_network_tokenization_enabled, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + is_pre_network_tokenization_enabled: self.is_pre_network_tokenization_enabled, }, ))) } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 54f775889e64..92d69f2af22e 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -454,11 +454,6 @@ where .get_payment_attempt() .customer_acceptance .clone(); - let customer_id = payment_data.get_payment_intent().customer_id.clone(); - let payment_method_data = payment_data.get_payment_method_data(); - let is_pre_tokenization_enabled = business_profile.is_network_tokenization_enabled - && business_profile.is_tokenize_before_payment_enabled - && customer_acceptance.is_some(); payment_data = match connector_details { ConnectorCallType::PreDetermined(connector) => { #[cfg(all(feature = "dynamic_routing", feature = "v1"))] @@ -479,61 +474,18 @@ where } else { None }; - let filtered_nt_supported_connectors = - get_filtered_nt_supported_connectors(&state, [connector.clone()].to_vec()); - let is_nt_supported_connector_available = - filtered_nt_supported_connectors.first().is_some(); - - if is_pre_tokenization_enabled && is_nt_supported_connector_available { - let pre_tokenization_response = tokenization::pre_payment_tokenization( + if is_pre_network_tokenization_enabled( + state, + &business_profile, + customer_acceptance, + connector.connector_name, + ) { + set_payment_method_data_for_pre_network_tokenization( state, - customer_id, - payment_method_data, + &mut payment_data, ) - .await?; - let pm_data = payment_data.get_payment_method_data(); - match pre_tokenization_response { - (Some(token_response), Some(_token_ref)) => { - let token_data = domain::NetworkTokenData { - token_number: token_response.authentication_details.token, - token_exp_month: token_response.token_details.exp_month, - token_exp_year: token_response.token_details.exp_year, - token_cryptogram: Some( - token_response.authentication_details.cryptogram, - ), - card_issuer: None, - card_network: Some(token_response.network), - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: None, - eci: token_response.eci, - }; - match pm_data { - Some(domain::PaymentMethodData::Card(card_data)) => { - let vault_data = VaultData { - card_data: card_data.clone(), - network_token_data: token_data.clone(), - }; - payment_data.set_vault_operation( - PaymentMethodDataAction::VaultData(vault_data.clone()), - ) - } - _ => (), - } - payment_data.set_payment_method_data(Some( - domain::PaymentMethodData::NetworkToken(token_data), - )); - } - _ => match pm_data { - Some(domain::PaymentMethodData::Card(card_data)) => payment_data - .set_vault_operation(PaymentMethodDataAction::SaveCardData( - card_data.clone(), - )), - _ => (), - }, - } + .await } let (router_data, mca) = call_connector_service( state, @@ -620,64 +572,21 @@ where .map_err(|e| logger::error!(routable_connector_error=?e)) .unwrap_or_default(); - let filtered_nt_supported_connectors = - get_filtered_nt_supported_connectors(&state, connectors.clone()); - let is_nt_supported_connector_available = - filtered_nt_supported_connectors.first().is_some(); - let mut connectors = connectors.into_iter(); let connector_data = get_connector_data(&mut connectors)?; - if is_pre_tokenization_enabled && is_nt_supported_connector_available { - let pre_tokenization_response = tokenization::pre_payment_tokenization( + if is_pre_network_tokenization_enabled( + state, + &business_profile, + customer_acceptance, + connector_data.connector_name, + ) { + set_payment_method_data_for_pre_network_tokenization( state, - customer_id, - payment_method_data, + &mut payment_data, ) - .await?; - let pm_data = payment_data.get_payment_method_data(); - match pre_tokenization_response { - (Some(token_response), Some(_token_ref)) => { - let token_data = domain::NetworkTokenData { - token_number: token_response.authentication_details.token, - token_exp_month: token_response.token_details.exp_month, - token_exp_year: token_response.token_details.exp_year, - token_cryptogram: Some( - token_response.authentication_details.cryptogram, - ), - card_issuer: None, - card_network: Some(token_response.network), - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: None, - eci: token_response.eci, - }; - match pm_data { - Some(domain::PaymentMethodData::Card(card_data)) => { - let vault_data = VaultData { - card_data: card_data.clone(), - network_token_data: token_data.clone(), - }; - payment_data.set_vault_operation( - PaymentMethodDataAction::VaultData(vault_data.clone()), - ) - } - _ => (), - } - payment_data.set_payment_method_data(Some( - domain::PaymentMethodData::NetworkToken(token_data), - )); - } - _ => match pm_data { - Some(domain::PaymentMethodData::Card(card_data)) => payment_data - .set_vault_operation(PaymentMethodDataAction::SaveCardData( - card_data.clone(), - )), - _ => (), - }, - } + .await; } let schedule_time = if should_add_task_to_process_tracker { @@ -4560,15 +4469,26 @@ where #[derive(Clone, serde::Serialize, Debug)] pub enum PaymentMethodDataAction { - SaveCardData(hyperswitch_domain_models::payment_method_data::Card), - SaveNetworkTokenData(hyperswitch_domain_models::payment_method_data::NetworkTokenData), - VaultData(VaultData), + SaveCardData(Box), + SaveCardAndNetworkTokenData(Box), } -#[derive(Clone, serde::Serialize, Debug)] -pub struct VaultData { +#[derive(Default, Clone, serde::Serialize, Debug)] +pub struct CardAndNetworkTokenData { pub card_data: hyperswitch_domain_models::payment_method_data::Card, + pub network_token: NetworkTokenDataForVault, +} + +#[derive(Default, Clone, serde::Serialize, Debug)] +pub struct NetworkTokenDataForVault { pub network_token_data: hyperswitch_domain_models::payment_method_data::NetworkTokenData, + pub network_token_req_ref_id: String, +} + +#[derive(Default, Clone, serde::Serialize, Debug)] +pub struct CardDataForVault { + pub card_data: hyperswitch_domain_models::payment_method_data::Card, + pub network_token_req_ref_id: Option, } #[derive(Clone, serde::Serialize, Debug)] @@ -5115,6 +5035,105 @@ where Ok(()) } +#[cfg(feature = "v1")] +pub fn is_pre_network_tokenization_enabled( + state: &SessionState, + business_profile: &domain::Profile, + customer_acceptance: Option>, + connector_name: enums::Connector, +) -> bool { + let ntid_supported_connectors = &state + .conf + .network_transaction_id_supported_connectors + .connector_list; + + let is_nt_supported_connector = ntid_supported_connectors.contains(&connector_name); + + business_profile.is_network_tokenization_enabled + && business_profile.is_pre_network_tokenization_enabled + && customer_acceptance.is_some() + && is_nt_supported_connector +} + +#[cfg(feature = "v1")] +impl From for domain::NetworkTokenData { + fn from(token_response: network_tokenization::TokenResponse) -> Self { + Self { + token_number: token_response.authentication_details.token, + token_exp_month: token_response.token_details.exp_month, + token_exp_year: token_response.token_details.exp_year, + token_cryptogram: Some(token_response.authentication_details.cryptogram), + card_issuer: None, + card_network: Some(token_response.network), + card_type: None, + card_issuing_country: None, + bank_code: None, + nick_name: None, + eci: token_response.eci, + } + } +} + +#[cfg(feature = "v1")] +pub async fn set_payment_method_data_for_pre_network_tokenization( + state: &SessionState, + payment_data: &mut D, +) +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ + let customer_id = payment_data.get_payment_intent().customer_id.clone(); + let payment_method_data = payment_data.get_payment_method_data(); + let pre_tokenization_response = + tokenization::pre_payment_tokenization(state, customer_id, payment_method_data) + .await + .ok(); + if let Some(domain::PaymentMethodData::Card(card_data)) = payment_method_data { + match pre_tokenization_response { + Some((Some(token_response), Some(token_ref))) => { + let token_data = domain::NetworkTokenData::from(token_response); + let network_token_data_for_vault = NetworkTokenDataForVault { + network_token_data: token_data.clone(), + network_token_req_ref_id: token_ref, + }; + + let card_and_network_token_data = CardAndNetworkTokenData { + card_data: card_data.clone(), + network_token: network_token_data_for_vault.clone(), + }; + payment_data.set_vault_operation( + PaymentMethodDataAction::SaveCardAndNetworkTokenData( + Box::new(card_and_network_token_data.clone()), + ), + ); + + payment_data.set_payment_method_data(Some( + domain::PaymentMethodData::NetworkToken(token_data), + )); + } + Some((None, Some(token_ref))) => { + let card_data_for_vault = CardDataForVault { + card_data: card_data.clone(), + network_token_req_ref_id: Some(token_ref), + }; + payment_data.set_vault_operation(PaymentMethodDataAction::SaveCardData( + Box::new(card_data_for_vault.clone()), + )) + } + _ => { + let card_data_for_vault = CardDataForVault { + card_data: card_data.clone(), + network_token_req_ref_id: None, + }; + payment_data.set_vault_operation(PaymentMethodDataAction::SaveCardData( + Box::new(card_data_for_vault.clone()), + )) + } + } + } +} + #[cfg(feature = "v1")] #[allow(clippy::too_many_arguments)] pub async fn get_connector_choice( @@ -5762,8 +5781,25 @@ where .get_required_value("payment_method_info")? .clone(); - let filtered_nt_supported_connectors = - get_filtered_nt_supported_connectors(&state, connectors.clone()); + //fetch connectors that support ntid flow + let ntid_supported_connectors = &state + .conf + .network_transaction_id_supported_connectors + .connector_list; + //filered connectors list with ntid_supported_connectors + let filtered_ntid_supported_connectors = + filter_ntid_supported_connectors(connectors.clone(), ntid_supported_connectors); + + //fetch connectors that support network tokenization flow + let network_tokenization_supported_connectors = &state + .conf + .network_tokenization_supported_connectors + .connector_list; + //filered connectors list with ntid_supported_connectors and network_tokenization_supported_connectors + let filtered_nt_supported_connectors = filter_network_tokenization_supported_connectors( + filtered_ntid_supported_connectors, + network_tokenization_supported_connectors, + ); let action_type = decide_action_type( state, @@ -6048,31 +6084,6 @@ pub fn filter_network_tokenization_supported_connectors( .collect() } -pub fn get_filtered_nt_supported_connectors( - state: &SessionState, - connectors: Vec, -) -> Vec { - //fetch connectors that support ntid flow - let ntid_supported_connectors = &state - .conf - .network_transaction_id_supported_connectors - .connector_list; - //filered connectors list with ntid_supported_connectors - let filtered_ntid_supported_connectors = - filter_ntid_supported_connectors(connectors.clone(), ntid_supported_connectors); - - //fetch connectors that support network tokenization flow - let network_tokenization_supported_connectors = &state - .conf - .network_tokenization_supported_connectors - .connector_list; - //filered connectors list with ntid_supported_connectors and network_tokenization_supported_connectors - filter_network_tokenization_supported_connectors( - filtered_ntid_supported_connectors, - network_tokenization_supported_connectors, - ) -} - #[cfg(feature = "v1")] pub async fn decide_action_type( state: &SessionState, @@ -7785,7 +7796,7 @@ impl OperationSessionSetters for PaymentConfirmData { } fn set_vault_operation(&mut self, vault_operation: PaymentMethodDataAction) { - self.vault_operation = Some(vault_operation); + todo!() } } @@ -8219,4 +8230,8 @@ impl OperationSessionSetters for PaymentCaptureData { fn set_connector_in_payment_attempt(&mut self, connector: Option) { todo!() } + + fn set_vault_operation(&mut self, vault_operation: PaymentMethodDataAction) { + todo!() + } } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 9692b1ca8a5c..7cd961ce663a 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -163,6 +163,8 @@ impl PostUpdateTracker, types::PaymentsAuthor .and_then(|billing_details| billing_details.address.as_ref()) .and_then(|address| address.get_optional_full_name()); let mut should_avoid_saving = false; + let vault_operation = payment_data.vault_operation.clone(); + let payment_method_info = payment_data.payment_method_info.clone(); if let Some(payment_method_info) = &payment_data.payment_method_info { if payment_data.payment_intent.off_session.is_none() && resp.response.is_ok() { @@ -201,6 +203,8 @@ impl PostUpdateTracker, types::PaymentsAuthor business_profile, connector_mandate_reference_id.clone(), merchant_connector_id.clone(), + vault_operation.clone(), + payment_method_info.clone(), )); let is_connector_mandate = resp.request.customer_acceptance.is_some() @@ -317,6 +321,8 @@ impl PostUpdateTracker, types::PaymentsAuthor &business_profile, connector_mandate_reference_id, merchant_connector_id.clone(), + vault_operation.clone(), + payment_method_info.clone(), )) .await; @@ -1083,7 +1089,8 @@ impl PostUpdateTracker, types::SetupMandateRequestDa .connector_mandate_detail .as_ref() .map(|detail| ConnectorMandateReferenceId::foreign_from(detail.clone())); - + let vault_operation = payment_data.vault_operation.clone(); + let payment_method_info = payment_data.payment_method_info.clone(); let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); let tokenization::SavePaymentMethodDataResponse { payment_method_id, @@ -1102,6 +1109,8 @@ impl PostUpdateTracker, types::SetupMandateRequestDa business_profile, connector_mandate_reference_id, merchant_connector_id.clone(), + vault_operation, + payment_method_info, )) .await?; diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 3b38df9919bc..e1d7984d52ec 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -86,6 +86,8 @@ pub async fn save_payment_method( business_profile: &domain::Profile, mut original_connector_mandate_reference_id: Option, merchant_connector_id: Option, + vault_operation: Option, + payment_method_info: Option, ) -> RouterResult where FData: mandate::MandateBehaviour + Clone, @@ -205,41 +207,215 @@ where .await?; ((res, dc, None), None) } else { - pm_status = Some(common_enums::PaymentMethodStatus::from( + let payment_method_status = common_enums::PaymentMethodStatus::from( save_payment_method_data.attempt_status, - )); - let (res, dc) = Box::pin(save_in_locker( - state, - merchant_account, - payment_method_create_request.to_owned(), - )) - .await?; + ); + pm_status = Some(payment_method_status); - if is_network_tokenization_enabled { - let pm_data = &save_payment_method_data.request.get_payment_method_data(); - match pm_data { - domain::PaymentMethodData::Card(card) => { - let ( - network_token_resp, - _network_token_duplication_check, //the duplication check is discarded, since each card has only one token, handling card duplication check will be suffice - network_token_requestor_ref_id, - ) = Box::pin(save_network_token_in_locker( + if let Some(payment_method_data_action) = vault_operation { + let network_token_requestor_reference_id = + payment_method_info.and_then(|pm_info| { + pm_info.network_token_requestor_reference_id.clone() + }); + match payment_method_data_action { + payments::PaymentMethodDataAction::SaveCardData(card) => { + let card_data = api::CardDetail { + card_number: card.card_data.card_number.clone(), + card_exp_month: card.card_data.card_exp_month.clone(), + card_exp_year: card.card_data.card_exp_year.clone(), + card_holder_name: None, + nick_name: None, + card_issuing_country: None, + card_network: card.card_data.card_network.clone(), + card_issuer: None, + card_type: None, + }; + if let (Some(nt_ref_id), Some(tokenization_service)) = ( + card.network_token_req_ref_id.clone(), + &state.conf.network_tokenization_service, + ) { + let _ = record_operation_time( + async { + network_tokenization::delete_network_token_from_tokenization_service( + state, + nt_ref_id.clone(), + &customer_id, + tokenization_service.get_inner(), + ) + .await + }, + &metrics::DELETE_NETWORK_TOKEN_TIME, + &[], + ) + .await; + } + let (res, dc) = Box::pin(save_in_locker( state, merchant_account, - card, - payment_method_create_request.clone(), + payment_method_create_request.to_owned(), + Some(card_data), )) - .await?; + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card In Locker Failed")?; - ( - (res, dc, network_token_requestor_ref_id), - network_token_resp, - ) + ((res, dc, None), None) + } + payments::PaymentMethodDataAction::SaveCardAndNetworkTokenData( + save_card_and_network_token_data, + ) => { + let card_data = api::CardDetail { + card_number: save_card_and_network_token_data + .card_data + .card_number + .clone(), + card_exp_month: save_card_and_network_token_data + .card_data + .card_exp_month + .clone(), + card_exp_year: save_card_and_network_token_data + .card_data + .card_exp_year + .clone(), + card_holder_name: None, + nick_name: None, + card_issuing_country: None, + card_network: save_card_and_network_token_data + .card_data + .card_network + .clone(), + card_issuer: None, + card_type: None, + }; + let network_token_data = api::CardDetail { + card_number: save_card_and_network_token_data + .network_token + .network_token_data + .token_number + .clone(), + card_exp_month: save_card_and_network_token_data + .network_token + .network_token_data + .token_exp_month + .clone(), + card_exp_year: save_card_and_network_token_data + .network_token + .network_token_data + .token_exp_year + .clone(), + card_holder_name: None, + nick_name: None, + card_issuing_country: None, + card_network: save_card_and_network_token_data + .network_token + .network_token_data + .card_network + .clone(), + card_issuer: None, + card_type: None, + }; + + if payment_method_status + == common_enums::PaymentMethodStatus::Active + { + let (res, dc) = Box::pin(save_in_locker( + state, + merchant_account, + payment_method_create_request.to_owned(), + Some(card_data), + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card In Locker Failed")?; + + let (network_token_resp, _dc, _) = Box::pin( + save_network_token_in_locker( + state, + merchant_account, + &save_card_and_network_token_data.card_data, + Some(network_token_data), + payment_method_create_request.clone(), + ), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Network Token In Locker Failed")?; + + ( + (res, dc, network_token_requestor_reference_id), + network_token_resp, + ) + } else { + if let (Some(nt_ref_id), Some(tokenization_service)) = ( + network_token_requestor_reference_id.clone(), + &state.conf.network_tokenization_service, + ) { + let _ = record_operation_time( + async { + network_tokenization::delete_network_token_from_tokenization_service( + state, + nt_ref_id.clone(), + &customer_id, + tokenization_service.get_inner(), + ) + .await + }, + &metrics::DELETE_NETWORK_TOKEN_TIME, + &[], + ) + .await; + } + let (res, dc) = Box::pin(save_in_locker( + state, + merchant_account, + payment_method_create_request.to_owned(), + Some(card_data), + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card In Locker Failed")?; + + ((res, dc, None), None) + } } - _ => ((res, dc, None), None), //network_token_resp is None in case of other payment methods } } else { - ((res, dc, None), None) + let (res, dc) = Box::pin(save_in_locker( + state, + merchant_account, + payment_method_create_request.to_owned(), + None, + )) + .await?; + + if is_network_tokenization_enabled { + let pm_data = + &save_payment_method_data.request.get_payment_method_data(); + match pm_data { + domain::PaymentMethodData::Card(card) => { + let ( + network_token_resp, + _network_token_duplication_check, //the duplication check is discarded, since each card has only one token, handling card duplication check will be suffice + network_token_requestor_ref_id, + ) = Box::pin(save_network_token_in_locker( + state, + merchant_account, + card, + None, + payment_method_create_request.clone(), + )) + .await?; + + ( + (res, dc, network_token_requestor_ref_id), + network_token_resp, + ) + } + _ => ((res, dc, None), None), //network_token_resp is None in case of other payment methods + } + } else { + ((res, dc, None), None) + } } }; let network_token_locker_id = match network_token_resp { @@ -810,6 +986,7 @@ where todo!() } +#[cfg(feature = "v1")] pub async fn pre_payment_tokenization( state: &SessionState, customer_id: Option, @@ -867,9 +1044,10 @@ pub async fn pre_payment_tokenization( Some(token_response), network_token_requestor_ref_id.clone(), )), - _ => Ok((None, None)), + _ => Ok((None, network_token_requestor_ref_id.clone())), } } + (Some(token_ref), _) => Ok((None, Some(token_ref))), _ => Ok((None, None)), } } @@ -997,6 +1175,7 @@ pub async fn save_in_locker( state: &SessionState, merchant_account: &domain::MerchantAccount, payment_method_request: api::PaymentMethodCreate, + card_detail: Option, ) -> RouterResult<( api_models::payment_methods::PaymentMethodResponse, Option, @@ -1007,8 +1186,8 @@ pub async fn save_in_locker( .customer_id .clone() .get_required_value("customer_id")?; - match payment_method_request.card.clone() { - Some(card) => Box::pin(payment_methods::cards::add_card_to_locker( + match (payment_method_request.card.clone(), card_detail) { + (_, Some(card)) | (Some(card), _) => Box::pin(payment_methods::cards::add_card_to_locker( state, payment_method_request, &card, @@ -1019,7 +1198,7 @@ pub async fn save_in_locker( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Add Card Failed"), - None => { + _ => { let pm_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); let payment_method_response = api::PaymentMethodResponse { merchant_id: merchant_id.clone(), @@ -1077,6 +1256,7 @@ pub async fn save_network_token_in_locker( state: &SessionState, merchant_account: &domain::MerchantAccount, card_data: &domain::Card, + network_token_data: Option, payment_method_request: api::PaymentMethodCreate, ) -> RouterResult<( Option, @@ -1092,54 +1272,73 @@ pub async fn save_network_token_in_locker( .network_tokenization_supported_card_networks .card_networks; - if card_data - .card_network - .as_ref() - .filter(|cn| network_tokenization_supported_card_networks.contains(cn)) - .is_some() - { - match network_tokenization::make_card_network_tokenization_request( - state, - card_data, - &customer_id, - ) - .await - { - Ok((token_response, network_token_requestor_ref_id)) => { - // Only proceed if the tokenization was successful - let network_token_data = api::CardDetail { - card_number: token_response.token.clone(), - card_exp_month: token_response.token_expiry_month.clone(), - card_exp_year: token_response.token_expiry_year.clone(), - card_holder_name: None, - nick_name: None, - card_issuing_country: None, - card_network: Some(token_response.card_brand.clone()), - card_issuer: None, - card_type: None, - }; - - let (res, dc) = Box::pin(payment_methods::cards::add_card_to_locker( + match network_token_data { + Some(nt_data) => { + let (res, dc) = Box::pin(payment_methods::cards::add_card_to_locker( + state, + payment_method_request, + &nt_data, + &customer_id, + merchant_account, + None, + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Network Token Failed")?; + + Ok((Some(res), dc, None)) + } + None => { + if card_data + .card_network + .as_ref() + .filter(|cn| network_tokenization_supported_card_networks.contains(cn)) + .is_some() + { + match network_tokenization::make_card_network_tokenization_request( state, - payment_method_request, - &network_token_data, + card_data, &customer_id, - merchant_account, - None, - )) + ) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Add Network Token Failed")?; + { + Ok((token_response, network_token_requestor_ref_id)) => { + // Only proceed if the tokenization was successful + let network_token_data = api::CardDetail { + card_number: token_response.token.clone(), + card_exp_month: token_response.token_expiry_month.clone(), + card_exp_year: token_response.token_expiry_year.clone(), + card_holder_name: None, + nick_name: None, + card_issuing_country: None, + card_network: Some(token_response.card_brand.clone()), + card_issuer: None, + card_type: None, + }; - Ok((Some(res), dc, network_token_requestor_ref_id)) - } - Err(err) => { - logger::error!("Failed to tokenize card: {:?}", err); - Ok((None, None, None)) //None will be returned in case of error when calling network tokenization service + let (res, dc) = Box::pin(payment_methods::cards::add_card_to_locker( + state, + payment_method_request, + &network_token_data, + &customer_id, + merchant_account, + None, + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Network Token Failed")?; + + Ok((Some(res), dc, network_token_requestor_ref_id)) + } + Err(err) => { + logger::error!("Failed to tokenize card: {:?}", err); + Ok((None, None, None)) //None will be returned in case of error when calling network tokenization service + } + } + } else { + Ok((None, None, None)) //None will be returned in case of unsupported card network. } } - } else { - Ok((None, None, None)) //None will be returned in case of unsupported card network. } } diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 70a5c8dad5c1..3a4220680b77 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -178,7 +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_tokenize_before_payment_enabled: item.is_tokenize_before_payment_enabled, + is_pre_network_tokenization_enabled: item.is_pre_network_tokenization_enabled, }) } } @@ -250,6 +250,7 @@ impl ForeignTryFrom for ProfileResponse { is_network_tokenization_enabled: item.is_network_tokenization_enabled, is_click_to_pay_enabled: item.is_click_to_pay_enabled, authentication_product_ids: item.authentication_product_ids, + is_pre_network_tokenization_enabled: item.is_pre_network_tokenization_enabled, }) } } @@ -378,6 +379,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: request.authentication_product_ids, - is_tokenize_before_payment_enabled: request.is_network_tokenization_enabled, + is_pre_network_tokenization_enabled: request.is_network_tokenization_enabled, })) } diff --git a/migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/down.sql b/migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/down.sql deleted file mode 100644 index 56559f452ee4..000000000000 --- a/migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE business_profile DROP COLUMN IF EXISTS is_tokenize_before_payment_enabled; \ No newline at end of file diff --git a/migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/up.sql b/migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/up.sql deleted file mode 100644 index d481781c3a8a..000000000000 --- a/migrations/2024-12-02-113838_add_is_tokenize_before_payment_enabled_in_business_profile/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS is_tokenize_before_payment_enabled BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/down.sql b/migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/down.sql new file mode 100644 index 000000000000..6d40bb557337 --- /dev/null +++ b/migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile DROP COLUMN IF EXISTS is_pre_network_tokenization_enabled; \ No newline at end of file diff --git a/migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/up.sql b/migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/up.sql new file mode 100644 index 000000000000..149471b0b8c9 --- /dev/null +++ b/migrations/2025-01-22-110625_add_is_pre_network_tokenization_enabled_in_business_profile/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS is_pre_network_tokenization_enabled BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file