diff --git a/config/config.example.toml b/config/config.example.toml index b2e1fe3c45..b5585a84b3 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -613,3 +613,6 @@ payment_attempts = "hyperswitch-payment-attempt-events" payment_intents = "hyperswitch-payment-intent-events" refunds = "hyperswitch-refund-events" disputes = "hyperswitch-dispute-events" + +[saved_payment_methods] +sdk_eligible_payment_methods = ["card"] \ No newline at end of file diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 8edfef7485..fe47b10b54 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -319,3 +319,6 @@ connectors_with_webhook_source_verification_call = "paypal" # List of co [unmasked_headers] keys = "user-agent" + +[saved_payment_methods] +sdk_eligible_payment_methods = ["card"] \ No newline at end of file diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 12724485ea..3135b3d568 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -330,3 +330,6 @@ connectors_with_webhook_source_verification_call = "paypal" # List of connec [unmasked_headers] keys = "user-agent" + +[saved_payment_methods] +sdk_eligible_payment_methods = ["card"] \ No newline at end of file diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 6c830c389a..921301ebd6 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -334,3 +334,6 @@ connectors_with_webhook_source_verification_call = "paypal" # List of con [unmasked_headers] keys = "user-agent" + +[saved_payment_methods] +sdk_eligible_payment_methods = ["card"] \ No newline at end of file diff --git a/config/development.toml b/config/development.toml index 92e13f24fe..db7f1ed93f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -615,3 +615,6 @@ payment_attempts = "hyperswitch-payment-attempt-events" payment_intents = "hyperswitch-payment-intent-events" refunds = "hyperswitch-refund-events" disputes = "hyperswitch-dispute-events" + +[saved_payment_methods] +sdk_eligible_payment_methods = ["card"] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index d99ab47350..702d134f2d 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -474,3 +474,6 @@ payment_attempts = "hyperswitch-payment-attempt-events" payment_intents = "hyperswitch-payment-intent-events" refunds = "hyperswitch-refund-events" disputes = "hyperswitch-dispute-events" + +[saved_payment_methods] +sdk_eligible_payment_methods = ["card"] \ No newline at end of file diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index e2807f3b5e..262848e1d5 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -143,6 +143,8 @@ pub enum PaymentMethodUpdate { status: Option, locker_id: Option, payment_method: Option, + payment_method_type: Option, + payment_method_issuer: Option, }, ConnectorMandateDetailsUpdate { connector_mandate_details: Option, @@ -162,6 +164,8 @@ pub struct PaymentMethodUpdateInternal { locker_id: Option, payment_method: Option, connector_mandate_details: Option, + payment_method_type: Option, + payment_method_issuer: Option, } impl PaymentMethodUpdateInternal { @@ -208,6 +212,8 @@ impl From for PaymentMethodUpdateInternal { locker_id: None, payment_method: None, connector_mandate_details: None, + payment_method_issuer: None, + payment_method_type: None, }, PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data, @@ -220,6 +226,8 @@ impl From for PaymentMethodUpdateInternal { locker_id: None, payment_method: None, connector_mandate_details: None, + payment_method_issuer: None, + payment_method_type: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { metadata: None, @@ -230,6 +238,8 @@ impl From for PaymentMethodUpdateInternal { locker_id: None, payment_method: None, connector_mandate_details: None, + payment_method_issuer: None, + payment_method_type: None, }, PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate { network_transaction_id, @@ -243,6 +253,8 @@ impl From for PaymentMethodUpdateInternal { locker_id: None, payment_method: None, connector_mandate_details: None, + payment_method_issuer: None, + payment_method_type: None, }, PaymentMethodUpdate::StatusUpdate { status } => Self { metadata: None, @@ -253,12 +265,16 @@ impl From for PaymentMethodUpdateInternal { locker_id: None, payment_method: None, connector_mandate_details: None, + payment_method_issuer: None, + payment_method_type: None, }, PaymentMethodUpdate::AdditionalDataUpdate { payment_method_data, status, locker_id, payment_method, + payment_method_type, + payment_method_issuer, } => Self { metadata: None, payment_method_data, @@ -268,6 +284,8 @@ impl From for PaymentMethodUpdateInternal { locker_id, payment_method, connector_mandate_details: None, + payment_method_issuer, + payment_method_type, }, PaymentMethodUpdate::ConnectorMandateDetailsUpdate { connector_mandate_details, @@ -280,6 +298,8 @@ impl From for PaymentMethodUpdateInternal { payment_method: None, connector_mandate_details, network_transaction_id: None, + payment_method_issuer: None, + payment_method_type: None, }, } } diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 537f6666c5..423f8e03d2 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -365,5 +365,6 @@ pub(crate) async fn fetch_raw_secrets( connector_onboarding, cors: conf.cors, unmasked_headers: conf.unmasked_headers, + saved_payment_methods: conf.saved_payment_methods, } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 01b05c60bd..12ce9c39d6 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -119,6 +119,7 @@ pub struct Settings { #[cfg(feature = "olap")] pub connector_onboarding: SecretStateContainer, pub unmasked_headers: UnmaskedHeaders, + pub saved_payment_methods: EligiblePaymentMethods, } #[derive(Debug, Deserialize, Clone, Default)] @@ -165,6 +166,11 @@ pub struct PaymentMethodAuth { pub pm_auth_key: Secret, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct EligiblePaymentMethods { + pub sdk_eligible_payment_methods: HashSet, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct DefaultExchangeRates { pub base_currency: String, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 27d126bce9..d3f6aeea24 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -441,6 +441,8 @@ pub async fn add_payment_method_data( status: Some(enums::PaymentMethodStatus::Active), locker_id: Some(locker_id), payment_method: req.payment_method, + payment_method_issuer: req.payment_method_issuer, + payment_method_type: req.payment_method_type, }; db.update_payment_method( @@ -555,7 +557,7 @@ pub async fn add_payment_method( match duplication_check { Some(duplication_check) => match duplication_check { payment_methods::DataDuplicationCheck::Duplicated => { - get_or_insert_payment_method( + let existing_pm = get_or_insert_payment_method( db, req.clone(), &mut resp, @@ -564,6 +566,8 @@ pub async fn add_payment_method( key_store, ) .await?; + + resp.client_secret = existing_pm.client_secret; } payment_methods::DataDuplicationCheck::MetaDataChanged => { if let Some(card) = req.card.clone() { @@ -577,6 +581,8 @@ pub async fn add_payment_method( ) .await?; + let client_secret = existing_pm.client_secret.clone(); + delete_card_from_locker( &state, &customer_id, @@ -653,6 +659,8 @@ pub async fn add_payment_method( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; + + resp.client_secret = client_secret; } } }, @@ -667,7 +675,7 @@ pub async fn add_payment_method( None }; resp.payment_method_id = generate_id(consts::ID_LENGTH, "pm"); - insert_payment_method( + let pm = insert_payment_method( db, &resp, req, @@ -682,6 +690,8 @@ pub async fn add_payment_method( merchant_account.storage_scheme, ) .await?; + + resp.client_secret = pm.client_secret; } } @@ -744,6 +754,18 @@ pub async fn update_customer_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + if pm.status == enums::PaymentMethodStatus::AwaitingData { + return Err(report!(errors::ApiErrorResponse::NotSupported { + message: "Payment method is awaiting data so it cannot be updated".into() + })); + } + + if pm.payment_method_data.is_none() { + return Err(report!(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment_method_data not found".to_string() + })); + } + // Fetch the existing payment method data from db let existing_card_data = decrypt::( pm.payment_method_data.clone(), @@ -812,7 +834,7 @@ pub async fn update_customer_payment_method( wallet: req.wallet, metadata: req.metadata, customer_id: Some(pm.customer_id.clone()), - client_secret: None, + client_secret: pm.client_secret.clone(), payment_method_data: None, card_network: req .card_network @@ -901,7 +923,7 @@ pub async fn update_customer_payment_method( installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), last_used_at: Some(common_utils::date_time::now()), - client_secret: None, + client_secret: pm.client_secret.clone(), } }; @@ -1691,12 +1713,21 @@ pub async fn list_payment_methods( let db = &*state.store; let pm_config_mapping = &state.conf.pm_filters; - let payment_intent = helpers::verify_payment_intent_time_and_client_secret( - db, - &merchant_account, - req.client_secret.clone(), - ) - .await?; + let payment_intent = if let Some(cs) = &req.client_secret { + if cs.starts_with("pm_") { + validate_payment_method_and_client_secret(cs, db, &merchant_account).await?; + None + } else { + helpers::verify_payment_intent_time_and_client_secret( + db, + &merchant_account, + req.client_secret.clone(), + ) + .await? + } + } else { + None + }; let shipping_address = payment_intent .as_ref() @@ -1839,6 +1870,7 @@ pub async fn list_payment_methods( pm_config_mapping, &state.conf.mandates.supported_payment_methods, &state.conf.mandates.update_mandate_supported, + &state.conf.saved_payment_methods, ) .await?; } @@ -2535,6 +2567,34 @@ pub async fn list_payment_methods( )) } +async fn validate_payment_method_and_client_secret( + cs: &String, + db: &dyn db::StorageInterface, + merchant_account: &domain::MerchantAccount, +) -> Result<(), error_stack::Report> { + let pm_vec = cs.split("_secret").collect::>(); + let pm_id = pm_vec + .first() + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "client_secret", + })?; + + let payment_method = db + .find_payment_method(pm_id, merchant_account.storage_scheme) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?; + + let client_secret_expired = + authenticate_pm_client_secret_and_check_expiry(cs, &payment_method)?; + if client_secret_expired { + return Err::<(), error_stack::Report>( + (errors::ApiErrorResponse::ClientSecretExpired).into(), + ); + } + Ok(()) +} + pub async fn call_surcharge_decision_management( state: routes::AppState, merchant_account: &domain::MerchantAccount, @@ -2644,6 +2704,7 @@ pub async fn filter_payment_methods( config: &settings::ConnectorFilters, supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate, supported_payment_methods_for_update_mandate: &settings::SupportedPaymentMethodsForMandate, + saved_payment_methods: &settings::EligiblePaymentMethods, ) -> errors::CustomResult<(), errors::ApiErrorResponse> { for payment_method in payment_methods.into_iter() { let parse_result = serde_json::from_value::(payment_method); @@ -2761,6 +2822,20 @@ pub async fn filter_payment_methods( }) .unwrap_or(true); + let filter9 = req + .client_secret + .as_ref() + .map(|cs| { + if cs.starts_with("pm_") { + saved_payment_methods + .sdk_eligible_payment_methods + .contains(payment_method.to_string().as_str()) + } else { + true + } + }) + .unwrap_or(true); + let connector = connector.clone(); let response_pm_type = ResponsePaymentMethodIntermediate::new( @@ -2777,6 +2852,7 @@ pub async fn filter_payment_methods( && filter6 && filter7 && filter8 + && filter9 { resp.push(response_pm_type); } diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 749508c974..9c53642f4d 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -374,7 +374,7 @@ pub fn mk_add_card_response_hs( installment_payment_enabled: false, // #[#256] payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), last_used_at: Some(common_utils::date_time::now()), // [#256] - client_secret: None, + client_secret: req.client_secret, } }