diff --git a/crates/api_models/src/apple_pay_certificates_migration.rs b/crates/api_models/src/apple_pay_certificates_migration.rs new file mode 100644 index 0000000000..796734f53e --- /dev/null +++ b/crates/api_models/src/apple_pay_certificates_migration.rs @@ -0,0 +1,12 @@ +#[derive(Debug, Clone, serde::Serialize)] +pub struct ApplePayCertificatesMigrationResponse { + pub migration_successful: Vec, + pub migration_failed: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct ApplePayCertificatesMigrationRequest { + pub merchant_ids: Vec, +} + +impl common_utils::events::ApiEventMetric for ApplePayCertificatesMigrationRequest {} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 9c26576e77..078c27e6db 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,3 +1,4 @@ +pub mod apple_pay_certificates_migration; pub mod connector_onboarding; pub mod customer; pub mod dispute; diff --git a/crates/api_models/src/events/apple_pay_certificates_migration.rs b/crates/api_models/src/events/apple_pay_certificates_migration.rs new file mode 100644 index 0000000000..f194443bea --- /dev/null +++ b/crates/api_models/src/events/apple_pay_certificates_migration.rs @@ -0,0 +1,9 @@ +use common_utils::events::ApiEventMetric; + +use crate::apple_pay_certificates_migration::ApplePayCertificatesMigrationResponse; + +impl ApiEventMetric for ApplePayCertificatesMigrationResponse { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::ApplePayCertificatesMigration) + } +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index a0bc6f6362..d6d6deaa23 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -2,6 +2,7 @@ pub mod admin; pub mod analytics; pub mod api_keys; +pub mod apple_pay_certificates_migration; pub mod blocklist; pub mod cards_info; pub mod conditional_configs; diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index cda13289aa..8ccfa14eaf 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4208,6 +4208,23 @@ pub struct SessionTokenInfo { pub initiative_context: String, #[schema(value_type = Option)] pub merchant_business_country: Option, + #[serde(flatten)] + pub payment_processing_details_at: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(tag = "payment_processing_details_at")] +pub enum PaymentProcessingDetailsAt { + Hyperswitch(PaymentProcessingDetails), + Connector, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, ToSchema)] +pub struct PaymentProcessingDetails { + #[schema(value_type = String)] + pub payment_processing_certificate: Secret, + #[schema(value_type = String)] + pub payment_processing_certificate_key: Secret, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index edb088032c..d8859d40fa 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2322,11 +2322,6 @@ pub enum ReconStatus { Active, Disabled, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ApplePayFlow { - Simplified, - Manual, -} #[derive( Clone, diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 8939e07a76..1052840dbc 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -50,6 +50,7 @@ pub enum ApiEventsType { // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, RustLocker, + ApplePayCertificatesMigration, FraudCheck, Recon, Dispute { diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index b386d52517..e5f55f88c9 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -67,7 +67,7 @@ pub enum ConnectorAuthType { #[derive(Debug, Deserialize, serde::Serialize, Clone)] #[serde(untagged)] pub enum ApplePayTomlConfig { - Standard(payments::ApplePayMetadata), + Standard(Box), Zen(ZenApplePay), } diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index e45ef00262..680e3dacc8 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -43,6 +43,7 @@ pub struct MerchantConnectorAccount { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, pub status: storage_enums::ConnectorStatus, + pub connector_wallets_details: Option, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -72,6 +73,7 @@ pub struct MerchantConnectorAccountNew { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, pub status: storage_enums::ConnectorStatus, + pub connector_wallets_details: Option, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -96,6 +98,7 @@ pub struct MerchantConnectorAccountUpdateInternal { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, pub status: Option, + pub connector_wallets_details: Option, } impl MerchantConnectorAccountUpdateInternal { diff --git a/crates/diesel_models/src/query/generics.rs b/crates/diesel_models/src/query/generics.rs index 0527ff3a18..682766679f 100644 --- a/crates/diesel_models/src/query/generics.rs +++ b/crates/diesel_models/src/query/generics.rs @@ -166,7 +166,8 @@ where } Err(DieselError::NotFound) => Err(report!(errors::DatabaseError::NotFound)) .attach_printable_lazy(|| format!("Error while updating {debug_values}")), - _ => Err(report!(errors::DatabaseError::Others)) + Err(error) => Err(error) + .change_context(errors::DatabaseError::Others) .attach_printable_lazy(|| format!("Error while updating {debug_values}")), } } @@ -252,7 +253,8 @@ where } Err(DieselError::NotFound) => Err(report!(errors::DatabaseError::NotFound)) .attach_printable_lazy(|| format!("Error while updating by ID {debug_values}")), - _ => Err(report!(errors::DatabaseError::Others)) + Err(error) => Err(error) + .change_context(errors::DatabaseError::Others) .attach_printable_lazy(|| format!("Error while updating by ID {debug_values}")), } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index ff5f6aef0e..3525730fc8 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -679,6 +679,7 @@ diesel::table! { applepay_verified_domains -> Nullable>>, pm_auth_config -> Nullable, status -> ConnectorStatus, + connector_wallets_details -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 065290b6b2..8dca0c86a2 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -22,6 +22,12 @@ pub enum PaymentMethodData { CardToken(CardToken), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ApplePayFlow { + Simplified(api_models::payments::PaymentProcessingDetails), + Manual, +} + impl PaymentMethodData { pub fn get_payment_method(&self) -> Option { match self { diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index b5d96a5c53..00e13f5fce 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, marker::PhantomData}; use common_utils::id_type; use masking::Secret; -use crate::payment_address::PaymentAddress; +use crate::{payment_address::PaymentAddress, payment_method_data}; #[derive(Debug, Clone)] pub struct RouterData { @@ -22,6 +22,7 @@ pub struct RouterData { pub address: PaymentAddress, pub auth_type: common_enums::enums::AuthenticationType, pub connector_meta_data: Option, + pub connector_wallets_details: Option, pub amount_captured: Option, pub access_token: Option, pub session_token: Option, @@ -56,7 +57,7 @@ pub struct RouterData { pub connector_http_status_code: Option, pub external_latency: Option, /// Contains apple pay flow type simplified or manual - pub apple_pay_flow: Option, + pub apple_pay_flow: Option, pub frm_metadata: Option, diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 8016e1411a..6ea139aef0 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -309,6 +309,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::FeatureMetadata, api_models::payments::ApplepayConnectorMetadataRequest, api_models::payments::SessionTokenInfo, + api_models::payments::PaymentProcessingDetailsAt, + api_models::payments::PaymentProcessingDetails, api_models::payments::SwishQrData, api_models::payments::AirwallexData, api_models::payments::NoonData, diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 02a5873429..53787fe04a 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod api_keys; pub mod api_locking; +pub mod apple_pay_certificates_migration; pub mod authentication; pub mod blocklist; pub mod cache; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 0ba807fd68..a06dded790 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -937,7 +937,7 @@ pub async fn create_payment_connector( payment_methods_enabled, test_mode: req.test_mode, disabled, - metadata: req.metadata, + metadata: req.metadata.clone(), frm_configs, connector_label: Some(connector_label.clone()), business_country: req.business_country, @@ -961,6 +961,7 @@ pub async fn create_payment_connector( applepay_verified_domains: None, pm_auth_config: req.pm_auth_config.clone(), status: connector_status, + connector_wallets_details: helpers::get_encrypted_apple_pay_connector_wallets_details(&key_store, &req.metadata).await?, }; let transaction_type = match req.connector_type { @@ -1200,6 +1201,7 @@ pub async fn update_payment_connector( expected_format: "auth_type and api_key".to_string(), })?; let metadata = req.metadata.clone().or(mca.metadata.clone()); + let connector_name = mca.connector_name.as_ref(); let connector_enum = api_models::enums::Connector::from_str(connector_name) .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -1275,6 +1277,10 @@ pub async fn update_payment_connector( applepay_verified_domains: None, pm_auth_config: req.pm_auth_config, status: Some(connector_status), + connector_wallets_details: helpers::get_encrypted_apple_pay_connector_wallets_details( + &key_store, &metadata, + ) + .await?, }; // Profile id should always be present diff --git a/crates/router/src/core/apple_pay_certificates_migration.rs b/crates/router/src/core/apple_pay_certificates_migration.rs new file mode 100644 index 0000000000..327358bda5 --- /dev/null +++ b/crates/router/src/core/apple_pay_certificates_migration.rs @@ -0,0 +1,109 @@ +use api_models::apple_pay_certificates_migration; +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use masking::{PeekInterface, Secret}; + +use super::{ + errors::{self, StorageErrorExt}, + payments::helpers, +}; +use crate::{ + routes::SessionState, + services::{self, logger}, + types::{domain::types as domain_types, storage}, +}; + +pub async fn apple_pay_certificates_migration( + state: SessionState, + req: &apple_pay_certificates_migration::ApplePayCertificatesMigrationRequest, +) -> CustomResult< + services::ApplicationResponse< + apple_pay_certificates_migration::ApplePayCertificatesMigrationResponse, + >, + errors::ApiErrorResponse, +> { + let db = state.store.as_ref(); + + let merchant_id_list = &req.merchant_ids; + + let mut migration_successful_merchant_ids = vec![]; + let mut migration_failed_merchant_ids = vec![]; + + for merchant_id in merchant_id_list { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let merchant_connector_accounts = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + true, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?; + + let mut mca_to_update = vec![]; + + for connector_account in merchant_connector_accounts { + let connector_apple_pay_metadata = + helpers::get_applepay_metadata(connector_account.clone().metadata) + .map_err(|error| { + logger::error!( + "Apple pay metadata parsing failed for {:?} in certificates migrations api {:?}", + connector_account.clone().connector_name, + error + ) + }) + .ok(); + if let Some(apple_pay_metadata) = connector_apple_pay_metadata { + let encrypted_apple_pay_metadata = domain_types::encrypt( + Secret::new( + serde_json::to_value(apple_pay_metadata) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to serialize apple pay metadata as JSON")?, + ), + key_store.key.get_inner().peek(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt connector apple pay metadata")?; + + let updated_mca = + storage::MerchantConnectorAccountUpdate::ConnectorWalletDetailsUpdate { + connector_wallets_details: encrypted_apple_pay_metadata, + }; + + mca_to_update.push((connector_account, updated_mca.into())); + } + } + + let merchant_connector_accounts_update = db + .update_multiple_merchant_connector_accounts(mca_to_update) + .await; + + match merchant_connector_accounts_update { + Ok(_) => { + logger::debug!("Merchant connector accounts updated for merchant id {merchant_id}"); + migration_successful_merchant_ids.push(merchant_id.to_string()); + } + Err(error) => { + logger::debug!( + "Merchant connector accounts update failed with error {error} for merchant id {merchant_id}"); + migration_failed_merchant_ids.push(merchant_id.to_string()); + } + }; + } + + Ok(services::api::ApplicationResponse::Json( + apple_pay_certificates_migration::ApplePayCertificatesMigrationResponse { + migration_successful: migration_successful_merchant_ids, + migration_failed: migration_failed_merchant_ids, + }, + )) +} diff --git a/crates/router/src/core/authentication/transformers.rs b/crates/router/src/core/authentication/transformers.rs index 2255e1ff58..704784c485 100644 --- a/crates/router/src/core/authentication/transformers.rs +++ b/crates/router/src/core/authentication/transformers.rs @@ -153,6 +153,7 @@ pub fn construct_router_data( address, auth_type: common_enums::AuthenticationType::NoThreeDs, connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: None, access_token: None, session_token: None, diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 74353e83b3..679a5dad29 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -67,6 +67,7 @@ impl ConstructFlowSpecificData( address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index b8f8810f09..ac661231ec 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -65,6 +65,7 @@ impl ConstructFlowSpecificData { - let domain_data = domain::PaymentMethodData::from(payment_data); - match domain_data { - domain::PaymentMethodData::Wallet(domain::WalletData::ApplePay( - wallet_data, - )) => Some( - ApplePayData::token_json(domain::WalletData::ApplePay(wallet_data)) - .change_context(errors::ApiErrorResponse::InternalServerError)? - .decrypt(state) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?, - ), - _ => None, + match &tokenization_action { + TokenizationAction::DecryptApplePayToken(payment_processing_details) + | TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt( + payment_processing_details, + ) => { + let apple_pay_data = match payment_data.payment_method_data.clone() { + Some(payment_method_data) => { + let domain_data = domain::PaymentMethodData::from(payment_method_data); + match domain_data { + domain::PaymentMethodData::Wallet(domain::WalletData::ApplePay( + wallet_data, + )) => Some( + ApplePayData::token_json(domain::WalletData::ApplePay(wallet_data)) + .change_context(errors::ApiErrorResponse::InternalServerError)? + .decrypt( + &payment_processing_details.payment_processing_certificate, + &payment_processing_details.payment_processing_certificate_key, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?, + ), + _ => None, + } } - } - _ => None, - }; + _ => None, + }; - let apple_pay_predecrypt = apple_pay_data - .parse_value::( - "ApplePayPredecryptData", - ) - .change_context(errors::ApiErrorResponse::InternalServerError)?; + let apple_pay_predecrypt = apple_pay_data + .parse_value::( + "ApplePayPredecryptData", + ) + .change_context(errors::ApiErrorResponse::InternalServerError)?; - logger::debug!(?apple_pay_predecrypt); - - router_data.payment_method_token = Some( - hyperswitch_domain_models::router_data::PaymentMethodToken::ApplePayDecrypt(Box::new( - apple_pay_predecrypt, - )), - ); - } + router_data.payment_method_token = Some( + hyperswitch_domain_models::router_data::PaymentMethodToken::ApplePayDecrypt( + Box::new(apple_pay_predecrypt), + ), + ); + } + _ => (), + }; let pm_token = router_data .add_payment_method_token(state, &connector, &tokenization_action) @@ -2055,7 +2058,7 @@ fn is_payment_method_tokenization_enabled_for_connector( connector_name: &str, payment_method: &storage::enums::PaymentMethod, payment_method_type: &Option, - apple_pay_flow: &Option, + apple_pay_flow: &Option, ) -> RouterResult { let connector_tokenization_filter = state.conf.tokenization.0.get(connector_name); @@ -2080,13 +2083,13 @@ fn is_payment_method_tokenization_enabled_for_connector( fn is_apple_pay_pre_decrypt_type_connector_tokenization( payment_method_type: &Option, - apple_pay_flow: &Option, + apple_pay_flow: &Option, apple_pay_pre_decrypt_flow_filter: Option, ) -> bool { match (payment_method_type, apple_pay_flow) { ( Some(storage::enums::PaymentMethodType::ApplePay), - Some(enums::ApplePayFlow::Simplified), + Some(domain::ApplePayFlow::Simplified(_)), ) => !matches!( apple_pay_pre_decrypt_flow_filter, Some(ApplePayPreDecryptFlow::NetworkTokenization) @@ -2096,18 +2099,22 @@ fn is_apple_pay_pre_decrypt_type_connector_tokenization( } fn decide_apple_pay_flow( + state: &SessionState, payment_method_type: &Option, merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, -) -> Option { +) -> Option { payment_method_type.and_then(|pmt| match pmt { - enums::PaymentMethodType::ApplePay => check_apple_pay_metadata(merchant_connector_account), + enums::PaymentMethodType::ApplePay => { + check_apple_pay_metadata(state, merchant_connector_account) + } _ => None, }) } fn check_apple_pay_metadata( + state: &SessionState, merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, -) -> Option { +) -> Option { merchant_connector_account.and_then(|mca| { let metadata = mca.get_metadata(); metadata.and_then(|apple_pay_metadata| { @@ -2141,14 +2148,34 @@ fn check_apple_pay_metadata( apple_pay_combined, ) => match apple_pay_combined { api_models::payments::ApplePayCombinedMetadata::Simplified { .. } => { - enums::ApplePayFlow::Simplified + domain::ApplePayFlow::Simplified(payments_api::PaymentProcessingDetails { + payment_processing_certificate: state + .conf + .applepay_decrypt_keys + .get_inner() + .apple_pay_ppc + .clone(), + payment_processing_certificate_key: state + .conf + .applepay_decrypt_keys + .get_inner() + .apple_pay_ppc_key + .clone(), + }) } - api_models::payments::ApplePayCombinedMetadata::Manual { .. } => { - enums::ApplePayFlow::Manual + api_models::payments::ApplePayCombinedMetadata::Manual { payment_request_data: _, session_token_data } => { + if let Some(manual_payment_processing_details_at) = session_token_data.payment_processing_details_at { + match manual_payment_processing_details_at { + payments_api::PaymentProcessingDetailsAt::Hyperswitch(payment_processing_details) => domain::ApplePayFlow::Simplified(payment_processing_details), + payments_api::PaymentProcessingDetailsAt::Connector => domain::ApplePayFlow::Manual, + } + } else { + domain::ApplePayFlow::Manual + } } }, api_models::payments::ApplepaySessionTokenMetadata::ApplePay(_) => { - enums::ApplePayFlow::Manual + domain::ApplePayFlow::Manual } }) }) @@ -2175,23 +2202,21 @@ async fn decide_payment_method_tokenize_action( payment_method: &storage::enums::PaymentMethod, pm_parent_token: Option<&String>, is_connector_tokenization_enabled: bool, - apple_pay_flow: Option, + apple_pay_flow: Option, ) -> RouterResult { - let is_apple_pay_predecrypt_supported = - matches!(apple_pay_flow, Some(enums::ApplePayFlow::Simplified)); - match pm_parent_token { - None => { - if is_connector_tokenization_enabled && is_apple_pay_predecrypt_supported { - Ok(TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt) - } else if is_connector_tokenization_enabled { - Ok(TokenizationAction::TokenizeInConnectorAndRouter) - } else if is_apple_pay_predecrypt_supported { - Ok(TokenizationAction::DecryptApplePayToken) - } else { - Ok(TokenizationAction::TokenizeInRouter) + None => Ok(match (is_connector_tokenization_enabled, apple_pay_flow) { + (true, Some(domain::ApplePayFlow::Simplified(payment_processing_details))) => { + TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt( + payment_processing_details, + ) } - } + (true, _) => TokenizationAction::TokenizeInConnectorAndRouter, + (false, Some(domain::ApplePayFlow::Simplified(payment_processing_details))) => { + TokenizationAction::DecryptApplePayToken(payment_processing_details) + } + (false, _) => TokenizationAction::TokenizeInRouter, + }), Some(token) => { let redis_conn = state .store @@ -2214,17 +2239,18 @@ async fn decide_payment_method_tokenize_action( match connector_token_option { Some(connector_token) => Ok(TokenizationAction::ConnectorToken(connector_token)), - None => { - if is_connector_tokenization_enabled && is_apple_pay_predecrypt_supported { - Ok(TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt) - } else if is_connector_tokenization_enabled { - Ok(TokenizationAction::TokenizeInConnectorAndRouter) - } else if is_apple_pay_predecrypt_supported { - Ok(TokenizationAction::DecryptApplePayToken) - } else { - Ok(TokenizationAction::TokenizeInRouter) + None => Ok(match (is_connector_tokenization_enabled, apple_pay_flow) { + (true, Some(domain::ApplePayFlow::Simplified(payment_processing_details))) => { + TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt( + payment_processing_details, + ) } - } + (true, _) => TokenizationAction::TokenizeInConnectorAndRouter, + (false, Some(domain::ApplePayFlow::Simplified(payment_processing_details))) => { + TokenizationAction::DecryptApplePayToken(payment_processing_details) + } + (false, _) => TokenizationAction::TokenizeInRouter, + }), } } } @@ -2237,8 +2263,8 @@ pub enum TokenizationAction { TokenizeInConnectorAndRouter, ConnectorToken(String), SkipConnectorTokenization, - DecryptApplePayToken, - TokenizeInConnectorAndApplepayPreDecrypt, + DecryptApplePayToken(payments_api::PaymentProcessingDetails), + TokenizeInConnectorAndApplepayPreDecrypt(payments_api::PaymentProcessingDetails), } #[allow(clippy::too_many_arguments)] @@ -2279,7 +2305,7 @@ where let payment_method_type = &payment_data.payment_attempt.payment_method_type; let apple_pay_flow = - decide_apple_pay_flow(payment_method_type, Some(merchant_connector_account)); + decide_apple_pay_flow(state, payment_method_type, Some(merchant_connector_account)); let is_connector_tokenization_enabled = is_payment_method_tokenization_enabled_for_connector( @@ -2348,12 +2374,14 @@ where TokenizationAction::SkipConnectorTokenization => { TokenizationAction::SkipConnectorTokenization } - TokenizationAction::DecryptApplePayToken => { - TokenizationAction::DecryptApplePayToken - } - TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt => { - TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt + TokenizationAction::DecryptApplePayToken(payment_processing_details) => { + TokenizationAction::DecryptApplePayToken(payment_processing_details) } + TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt( + payment_processing_details, + ) => TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt( + payment_processing_details, + ), }; (payment_data.to_owned(), connector_tokenization_action) } diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 76441f4075..14a3b9327d 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -166,8 +166,20 @@ async fn create_applepay_session_token( ) } else { // Get the apple pay metadata - let apple_pay_metadata = - helpers::get_applepay_metadata(router_data.connector_meta_data.clone())?; + let connector_apple_pay_wallet_details = + helpers::get_applepay_metadata(router_data.connector_wallets_details.clone()) + .map_err(|error| { + logger::debug!( + "Apple pay connector wallets details parsing failed in create_applepay_session_token {:?}", + error + ) + }) + .ok(); + + let apple_pay_metadata = match connector_apple_pay_wallet_details { + Some(apple_pay_wallet_details) => apple_pay_wallet_details, + None => helpers::get_applepay_metadata(router_data.connector_meta_data.clone())?, + }; // Get payment request data , apple pay session request and merchant keys let ( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d11d0ef3c8..3a9e1e4b45 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -6,6 +6,7 @@ use api_models::{ }; use base64::Engine; use common_utils::{ + crypto::Encryptable, ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt}, fp_utils, generate_id, id_type, pii, types::MinorUnit, @@ -3169,6 +3170,13 @@ impl MerchantConnectorAccountType { } } + pub fn get_connector_wallets_details(&self) -> Option> { + match self { + Self::DbVal(val) => val.connector_wallets_details.as_deref().cloned(), + Self::CacheVal(_) => None, + } + } + pub fn is_disabled(&self) -> bool { match self { Self::DbVal(ref inner) => inner.disabled.unwrap_or(false), @@ -3368,6 +3376,7 @@ pub fn router_data_type_conversion( refund_id: router_data.refund_id, dispute_id: router_data.dispute_id, connector_response: router_data.connector_response, + connector_wallets_details: router_data.connector_wallets_details, } } @@ -3905,17 +3914,32 @@ pub fn validate_customer_access( pub fn is_apple_pay_simplified_flow( connector_metadata: Option, + connector_wallets_details: Option, connector_name: Option<&String>, ) -> CustomResult { - let option_apple_pay_metadata = get_applepay_metadata(connector_metadata) - .map_err(|error| { - logger::info!( - "Apple pay metadata parsing for {:?} in is_apple_pay_simplified_flow {:?}", + let connector_apple_pay_wallet_details = + get_applepay_metadata(connector_wallets_details) + .map_err(|error| { + logger::debug!( + "Apple pay connector wallets details parsing failed for {:?} in is_apple_pay_simplified_flow {:?}", + connector_name, + error + ) + }) + .ok(); + + let option_apple_pay_metadata = match connector_apple_pay_wallet_details { + Some(apple_pay_wallet_details) => Some(apple_pay_wallet_details), + None => get_applepay_metadata(connector_metadata) + .map_err(|error| { + logger::debug!( + "Apple pay metadata parsing failed for {:?} in is_apple_pay_simplified_flow {:?}", connector_name, error ) - }) - .ok(); + }) + .ok(), + }; // return true only if the apple flow type is simplified Ok(matches!( @@ -3928,6 +3952,38 @@ pub fn is_apple_pay_simplified_flow( )) } +pub async fn get_encrypted_apple_pay_connector_wallets_details( + key_store: &domain::MerchantKeyStore, + connector_metadata: &Option>, +) -> RouterResult>>> { + let apple_pay_metadata = get_applepay_metadata(connector_metadata.clone()) + .map_err(|error| { + logger::error!( + "Apple pay metadata parsing failed in get_encrypted_apple_pay_connector_wallets_details {:?}", + error + ) + }) + .ok(); + + let connector_apple_pay_details = apple_pay_metadata + .map(|metadata| { + serde_json::to_value(metadata) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to serialize apple pay metadata as JSON") + }) + .transpose()? + .map(masking::Secret::new); + + let encrypted_connector_apple_pay_details = connector_apple_pay_details + .async_lift(|wallets_details| { + types::encrypt_optional(wallets_details, key_store.key.get_inner().peek()) + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting connector wallets details")?; + Ok(encrypted_connector_apple_pay_details) +} + pub fn get_applepay_metadata( connector_metadata: Option, ) -> RouterResult { @@ -3991,6 +4047,7 @@ where let connector_data_list = if is_apple_pay_simplified_flow( merchant_connector_account_type.get_metadata(), + merchant_connector_account_type.get_connector_wallets_details(), merchant_connector_account_type .get_connector_name() .as_ref(), @@ -4010,6 +4067,10 @@ where for merchant_connector_account in merchant_connector_account_list { if is_apple_pay_simplified_flow( merchant_connector_account.metadata, + merchant_connector_account + .connector_wallets_details + .as_deref() + .cloned(), Some(&merchant_connector_account.connector_name), )? { let connector_data = api::ConnectorData::get_connector_by_name( @@ -4064,10 +4125,11 @@ impl ApplePayData { pub async fn decrypt( &self, - state: &SessionState, + payment_processing_certificate: &masking::Secret, + payment_processing_certificate_key: &masking::Secret, ) -> CustomResult { - let merchant_id = self.merchant_id(state).await?; - let shared_secret = self.shared_secret(state).await?; + let merchant_id = self.merchant_id(payment_processing_certificate)?; + let shared_secret = self.shared_secret(payment_processing_certificate_key)?; let symmetric_key = self.symmetric_key(&merchant_id, &shared_secret)?; let decrypted = self.decrypt_ciphertext(&symmetric_key)?; let parsed_decrypted: serde_json::Value = serde_json::from_str(&decrypted) @@ -4075,17 +4137,11 @@ impl ApplePayData { Ok(parsed_decrypted) } - pub async fn merchant_id( + pub fn merchant_id( &self, - state: &SessionState, + payment_processing_certificate: &masking::Secret, ) -> CustomResult { - let cert_data = state - .conf - .applepay_decrypt_keys - .get_inner() - .apple_pay_ppc - .clone() - .expose(); + let cert_data = payment_processing_certificate.clone().expose(); let base64_decode_cert_data = BASE64_ENGINE .decode(cert_data) @@ -4120,9 +4176,9 @@ impl ApplePayData { Ok(apple_pay_m_id) } - pub async fn shared_secret( + pub fn shared_secret( &self, - state: &SessionState, + payment_processing_certificate_key: &masking::Secret, ) -> CustomResult, errors::ApplePayDecryptionError> { let public_ec_bytes = BASE64_ENGINE .decode(self.header.ephemeral_public_key.peek().as_bytes()) @@ -4132,13 +4188,7 @@ impl ApplePayData { .change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed) .attach_printable("Failed to deserialize the public key")?; - let decrypted_apple_pay_ppc_key = state - .conf - .applepay_decrypt_keys - .get_inner() - .apple_pay_ppc_key - .clone() - .expose(); + let decrypted_apple_pay_ppc_key = payment_processing_certificate_key.clone().expose(); // Create PKey objects from EcKey let private_key = PKey::private_key_from_pem(decrypted_apple_pay_ppc_key.as_bytes()) diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 48637a0d2b..7016c7542b 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -727,7 +727,7 @@ pub async fn add_payment_method_token( ) -> RouterResult> { match tokenization_action { payments::TokenizationAction::TokenizeInConnector - | payments::TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt => { + | payments::TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(_) => { let connector_integration: services::BoxedConnectorIntegration< '_, api::PaymentMethodToken, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index fa1d68ca23..959724696a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -125,6 +125,7 @@ where }; let apple_pay_flow = payments::decide_apple_pay_flow( + state, &payment_data.payment_attempt.payment_method_type, Some(merchant_connector_account), ); @@ -154,6 +155,7 @@ where .authentication_type .unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), request: T::try_from(additional_data)?, response, amount_captured: payment_data diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 1071e5c98d..3c4a119b9c 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -164,6 +164,7 @@ pub async fn construct_payout_router_data<'a, F>( address, auth_type: enums::AuthenticationType::default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: None, payment_method_status: None, request: types::PayoutsData { @@ -316,6 +317,7 @@ pub async fn construct_refund_router_data<'a, F>( address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), @@ -565,6 +567,7 @@ pub async fn construct_accept_dispute_router_data<'a>( address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), @@ -661,6 +664,7 @@ pub async fn construct_submit_evidence_router_data<'a>( address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), @@ -755,6 +759,7 @@ pub async fn construct_upload_file_router_data<'a>( address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), @@ -853,6 +858,7 @@ pub async fn construct_defend_dispute_router_data<'a>( address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), @@ -942,6 +948,7 @@ pub async fn construct_retrieve_file_router_data<'a>( address: PaymentAddress::default(), auth_type: diesel_models::enums::AuthenticationType::default(), connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: None, payment_method_status: None, request: types::RetrieveFileRequestData { diff --git a/crates/router/src/core/verification/utils.rs b/crates/router/src/core/verification/utils.rs index 7518091ee8..bfbc1cb8b4 100644 --- a/crates/router/src/core/verification/utils.rs +++ b/crates/router/src/core/verification/utils.rs @@ -61,6 +61,7 @@ pub async fn check_existence_and_add_domain_to_db( pm_auth_config: None, connector_label: None, status: None, + connector_wallets_details: None, }; state .store diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index ad1aafd312..1394c68cfa 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -86,6 +86,7 @@ pub async fn construct_webhook_router_data<'a>( address: PaymentAddress::default(), auth_type: diesel_models::enums::AuthenticationType::default(), connector_meta_data: None, + connector_wallets_details: None, amount_captured: None, request: types::VerifyWebhookSourceRequestData { webhook_headers: request_details.headers.clone(), diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index ea4a4180bc..c360b41a6c 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -942,6 +942,17 @@ impl FileMetadataInterface for KafkaStore { #[async_trait::async_trait] impl MerchantConnectorAccountInterface for KafkaStore { + async fn update_multiple_merchant_connector_accounts( + &self, + merchant_connector_accounts: Vec<( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), errors::StorageError> { + self.diesel_store + .update_multiple_merchant_connector_accounts(merchant_connector_accounts) + .await + } async fn find_merchant_connector_account_by_merchant_id_connector_label( &self, merchant_id: &str, diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index 0614325089..a176db2654 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -1,4 +1,6 @@ +use async_bb8_diesel::AsyncConnection; use common_utils::ext_traits::{AsyncExt, ByteSliceExt, Encode}; +use diesel_models::encryption::Encryption; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; #[cfg(feature = "accounts_cache")] @@ -165,6 +167,14 @@ where key_store: &domain::MerchantKeyStore, ) -> CustomResult; + async fn update_multiple_merchant_connector_accounts( + &self, + this: Vec<( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), errors::StorageError>; + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( &self, merchant_id: &str, @@ -381,6 +391,104 @@ impl MerchantConnectorAccountInterface for Store { .await } + #[instrument(skip_all)] + async fn update_multiple_merchant_connector_accounts( + &self, + merchant_connector_accounts: Vec<( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + + async fn update_call( + connection: &diesel_models::PgPooledConn, + (merchant_connector_account, mca_update): ( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + ), + ) -> Result<(), error_stack::Report> { + Conversion::convert(merchant_connector_account) + .await + .change_context(errors::StorageError::EncryptionError)? + .update(connection, mca_update) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + Ok(()) + } + + conn.transaction_async(|connection_pool| async move { + for (merchant_connector_account, update_merchant_connector_account) in + merchant_connector_accounts + { + let _connector_name = merchant_connector_account.connector_name.clone(); + let _profile_id = merchant_connector_account.profile_id.clone().ok_or( + errors::StorageError::ValueNotFound("profile_id".to_string()), + )?; + + let _merchant_id = merchant_connector_account.merchant_id.clone(); + let _merchant_connector_id = + merchant_connector_account.merchant_connector_id.clone(); + + let update = update_call( + &connection_pool, + ( + merchant_connector_account, + update_merchant_connector_account, + ), + ); + + #[cfg(feature = "accounts_cache")] + // Redact all caches as any of might be used because of backwards compatibility + cache::publish_and_redact_multiple( + self, + [ + cache::CacheKind::Accounts( + format!("{}_{}", _profile_id, _connector_name).into(), + ), + cache::CacheKind::Accounts( + format!("{}_{}", _merchant_id, _merchant_connector_id).into(), + ), + cache::CacheKind::CGraph( + format!("cgraph_{}_{_profile_id}", _merchant_id).into(), + ), + ], + || update, + ) + .await + .map_err(|error| { + // Returning `DatabaseConnectionError` after logging the actual error because + // -> it is not possible to get the underlying from `error_stack::Report` + // -> it is not possible to write a `From` impl to convert the `diesel::result::Error` to `error_stack::Report` + // because of Rust's orphan rules + router_env::logger::error!( + ?error, + "DB transaction for updating multiple merchant connector account failed" + ); + errors::StorageError::DatabaseConnectionError + })?; + + #[cfg(not(feature = "accounts_cache"))] + { + update.await.map_err(|error| { + // Returning `DatabaseConnectionError` after logging the actual error because + // -> it is not possible to get the underlying from `error_stack::Report` + // -> it is not possible to write a `From` impl to convert the `diesel::result::Error` to `error_stack::Report` + // because of Rust's orphan rules + router_env::logger::error!( + ?error, + "DB transaction for updating multiple merchant connector account failed" + ); + errors::StorageError::DatabaseConnectionError + })?; + } + } + Ok::<_, errors::StorageError>(()) + }) + .await?; + Ok(()) + } + #[instrument(skip_all)] async fn update_merchant_connector_account( &self, @@ -417,7 +525,7 @@ impl MerchantConnectorAccountInterface for Store { #[cfg(feature = "accounts_cache")] { - // Redact both the caches as any one or both might be used because of backwards compatibility + // Redact all caches as any of might be used because of backwards compatibility cache::publish_and_redact_multiple( self, [ @@ -508,6 +616,17 @@ impl MerchantConnectorAccountInterface for Store { #[async_trait::async_trait] impl MerchantConnectorAccountInterface for MockDb { + async fn update_multiple_merchant_connector_accounts( + &self, + _merchant_connector_accounts: Vec<( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), errors::StorageError> { + // No need to implement this function for `MockDb` as this function will be removed after the + // apple pay certificate migration + Err(errors::StorageError::MockDbError)? + } async fn find_merchant_connector_account_by_merchant_id_connector_label( &self, merchant_id: &str, @@ -664,6 +783,7 @@ impl MerchantConnectorAccountInterface for MockDb { applepay_verified_domains: t.applepay_verified_domains, pm_auth_config: t.pm_auth_config, status: t.status, + connector_wallets_details: t.connector_wallets_details.map(Encryption::from), }; accounts.push(account.clone()); account @@ -863,6 +983,14 @@ mod merchant_connector_account_cache_tests { applepay_verified_domains: None, pm_auth_config: None, status: common_enums::ConnectorStatus::Inactive, + connector_wallets_details: Some( + domain::types::encrypt( + serde_json::Value::default().into(), + merchant_key.key.get_inner().peek(), + ) + .await + .unwrap(), + ), }; db.insert_merchant_connector_account(mca.clone(), &merchant_key) diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 7344b85d15..73ec0f1635 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -144,6 +144,7 @@ pub fn mk_app( .service(routes::Routing::server(state.clone())) .service(routes::Blocklist::server(state.clone())) .service(routes::Gsm::server(state.clone())) + .service(routes::ApplePayCertificatesMigration::server(state.clone())) .service(routes::PaymentLink::server(state.clone())) .service(routes::User::server(state.clone())) .service(routes::ConnectorOnboarding::server(state.clone())) diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 9f13635ca9..cb229b5c80 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod api_keys; pub mod app; +pub mod apple_pay_certificates_migration; #[cfg(feature = "olap")] pub mod blocklist; pub mod cache; @@ -58,10 +59,10 @@ pub use self::app::Payouts; #[cfg(all(feature = "olap", feature = "recon"))] pub use self::app::Recon; pub use self::app::{ - ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, ConnectorOnboarding, Customers, - Disputes, EphemeralKey, Files, Gsm, Health, Mandates, MerchantAccount, - MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, Refunds, SessionState, - User, Webhooks, + ApiKeys, AppState, ApplePayCertificatesMigration, BusinessProfile, Cache, Cards, Configs, + ConnectorOnboarding, Customers, Disputes, EphemeralKey, Files, Gsm, Health, Mandates, + MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, + Refunds, SessionState, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 2d1b77460a..cd39275650 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -413,7 +413,7 @@ pub async fn payment_connector_delete( merchant_connector_id, }) .into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -430,7 +430,7 @@ pub async fn payment_connector_delete( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Account - Toggle KV diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 587ecd9e63..83d8c714e9 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -30,8 +30,8 @@ use super::routing as cloud_routing; use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] use super::{ - admin::*, api_keys::*, connector_onboarding::*, disputes::*, files::*, gsm::*, payment_link::*, - user::*, user_role::*, webhook_events::*, + admin::*, api_keys::*, apple_pay_certificates_migration, connector_onboarding::*, disputes::*, + files::*, gsm::*, payment_link::*, user::*, user_role::*, webhook_events::*, }; use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] @@ -1142,6 +1142,19 @@ impl Configs { } } +pub struct ApplePayCertificatesMigration; + +#[cfg(feature = "olap")] +impl ApplePayCertificatesMigration { + pub fn server(state: AppState) -> Scope { + web::scope("/apple_pay_certificates_migration") + .app_data(web::Data::new(state)) + .service(web::resource("").route( + web::post().to(apple_pay_certificates_migration::apple_pay_certificates_migration), + )) + } +} + pub struct Poll; #[cfg(feature = "oltp")] diff --git a/crates/router/src/routes/apple_pay_certificates_migration.rs b/crates/router/src/routes/apple_pay_certificates_migration.rs new file mode 100644 index 0000000000..8c9d507ba4 --- /dev/null +++ b/crates/router/src/routes/apple_pay_certificates_migration.rs @@ -0,0 +1,30 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, apple_pay_certificates_migration}, + services::{api, authentication as auth}, +}; + +pub async fn apple_pay_certificates_migration( + state: web::Data, + req: HttpRequest, + json_payload: web::Json< + api_models::apple_pay_certificates_migration::ApplePayCertificatesMigrationRequest, + >, +) -> HttpResponse { + let flow = Flow::ApplePayCertificatesMigration; + Box::pin(api::server_wrap( + flow, + state, + &req, + &json_payload.into_inner(), + |state, _, req, _| { + apple_pay_certificates_migration::apple_pay_certificates_migration(state, req) + }, + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b4b9658a7e..a64343757b 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -35,6 +35,7 @@ pub enum ApiIdentifier { ConnectorOnboarding, Recon, Poll, + ApplePayCertificatesMigration, } impl From for ApiIdentifier { @@ -186,6 +187,8 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, + Flow::ApplePayCertificatesMigration => Self::ApplePayCertificatesMigration, + Flow::UserConnectAccount | Flow::UserSignUp | Flow::UserSignIn diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 35d1f989d7..ff2df97f28 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -805,6 +805,7 @@ impl ForeignFrom<(&RouterData, T2) address: data.address.clone(), auth_type: data.auth_type, connector_meta_data: data.connector_meta_data.clone(), + connector_wallets_details: data.connector_wallets_details.clone(), amount_captured: data.amount_captured, access_token: data.access_token.clone(), response: data.response.clone(), @@ -865,6 +866,7 @@ impl address: data.address.clone(), auth_type: data.auth_type, connector_meta_data: data.connector_meta_data.clone(), + connector_wallets_details: data.connector_wallets_details.clone(), amount_captured: data.amount_captured, access_token: data.access_token.clone(), response: data.response.clone(), diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 0b932f9aeb..7e296d0b4a 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -85,6 +85,7 @@ impl VerifyConnectorData { connector_customer: None, connector_auth_type: self.connector_auth.clone(), connector_meta_data: None, + connector_wallets_details: None, payment_method_token: None, connector_api_version: None, recurring_mandate_payment_data: None, diff --git a/crates/router/src/types/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index 0e7f5b081c..6c2f6a06e1 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -11,7 +11,10 @@ use diesel_models::{ use error_stack::ResultExt; use masking::{PeekInterface, Secret}; -use super::{behaviour, types::TypeEncryption}; +use super::{ + behaviour, + types::{self, AsyncLift, TypeEncryption}, +}; #[derive(Clone, Debug)] pub struct MerchantConnectorAccount { pub id: Option, @@ -36,6 +39,7 @@ pub struct MerchantConnectorAccount { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, pub status: enums::ConnectorStatus, + pub connector_wallets_details: Option>>, } #[derive(Debug)] @@ -56,6 +60,10 @@ pub enum MerchantConnectorAccountUpdate { pm_auth_config: Option, connector_label: Option, status: Option, + connector_wallets_details: Option>>, + }, + ConnectorWalletDetailsUpdate { + connector_wallets_details: Encryptable>, }, } @@ -92,6 +100,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, status: self.status, + connector_wallets_details: self.connector_wallets_details.map(Encryption::from), }, ) } @@ -132,6 +141,13 @@ impl behaviour::Conversion for MerchantConnectorAccount { applepay_verified_domains: other.applepay_verified_domains, pm_auth_config: other.pm_auth_config, status: other.status, + connector_wallets_details: other + .connector_wallets_details + .async_lift(|inner| types::decrypt(inner, key.peek())) + .await + .change_context(ValidationError::InvalidValue { + message: "Failed while decrypting connector wallets details".to_string(), + })?, }) } @@ -160,6 +176,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, status: self.status, + connector_wallets_details: self.connector_wallets_details.map(Encryption::from), }) } } @@ -183,6 +200,7 @@ impl From for MerchantConnectorAccountUpdateInte pm_auth_config, connector_label, status, + connector_wallets_details, } => Self { merchant_id, connector_type, @@ -201,6 +219,29 @@ impl From for MerchantConnectorAccountUpdateInte pm_auth_config, connector_label, status, + connector_wallets_details: connector_wallets_details.map(Encryption::from), + }, + MerchantConnectorAccountUpdate::ConnectorWalletDetailsUpdate { + connector_wallets_details, + } => Self { + connector_wallets_details: Some(Encryption::from(connector_wallets_details)), + merchant_id: None, + connector_type: None, + connector_name: None, + connector_account_details: None, + connector_label: None, + test_mode: None, + disabled: None, + merchant_connector_id: None, + payment_methods_enabled: None, + frm_configs: None, + metadata: None, + modified_at: None, + connector_webhook_details: None, + frm_config: None, + applepay_verified_domains: None, + pm_auth_config: None, + status: None, }, } } diff --git a/crates/router/src/types/domain/payments.rs b/crates/router/src/types/domain/payments.rs index 51d3210a70..021505c644 100644 --- a/crates/router/src/types/domain/payments.rs +++ b/crates/router/src/types/domain/payments.rs @@ -1,10 +1,10 @@ pub use hyperswitch_domain_models::payment_method_data::{ - AliPayQr, ApplePayThirdPartySdkData, ApplePayWalletData, ApplepayPaymentMethod, BankDebitData, - BankRedirectData, BankTransferData, BoletoVoucherData, Card, CardRedirectData, CardToken, - CashappQr, CryptoData, GcashRedirection, GiftCardData, GiftCardDetails, GoPayRedirection, - GooglePayPaymentMethodInfo, GooglePayRedirectData, GooglePayThirdPartySdkData, - GooglePayWalletData, GpayTokenizationData, IndomaretVoucherData, KakaoPayRedirection, - MbWayRedirection, PayLaterData, PaymentMethodData, SamsungPayWalletData, + AliPayQr, ApplePayFlow, ApplePayThirdPartySdkData, ApplePayWalletData, ApplepayPaymentMethod, + BankDebitData, BankRedirectData, BankTransferData, BoletoVoucherData, Card, CardRedirectData, + CardToken, CashappQr, CryptoData, GcashRedirection, GiftCardData, GiftCardDetails, + GoPayRedirection, GooglePayPaymentMethodInfo, GooglePayRedirectData, + GooglePayThirdPartySdkData, GooglePayWalletData, GpayTokenizationData, IndomaretVoucherData, + KakaoPayRedirection, MbWayRedirection, PayLaterData, PaymentMethodData, SamsungPayWalletData, SepaAndBacsBillingDetails, SwishQrData, TouchNGoRedirection, UpiCollectData, UpiData, UpiIntentData, VoucherData, WalletData, WeChatPayQr, }; diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 328d4fbdc6..701eda5d1c 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -772,13 +772,13 @@ impl CustomerAddress for api_models::customers::CustomerRequest { } pub fn add_apple_pay_flow_metrics( - apple_pay_flow: &Option, + apple_pay_flow: &Option, connector: Option, merchant_id: String, ) { if let Some(flow) = apple_pay_flow { match flow { - enums::ApplePayFlow::Simplified => metrics::APPLE_PAY_SIMPLIFIED_FLOW.add( + domain::ApplePayFlow::Simplified(_) => metrics::APPLE_PAY_SIMPLIFIED_FLOW.add( &metrics::CONTEXT, 1, &[ @@ -789,7 +789,7 @@ pub fn add_apple_pay_flow_metrics( metrics::request::add_attributes("merchant_id", merchant_id.to_owned()), ], ), - enums::ApplePayFlow::Manual => metrics::APPLE_PAY_MANUAL_FLOW.add( + domain::ApplePayFlow::Manual => metrics::APPLE_PAY_MANUAL_FLOW.add( &metrics::CONTEXT, 1, &[ @@ -806,14 +806,14 @@ pub fn add_apple_pay_flow_metrics( pub fn add_apple_pay_payment_status_metrics( payment_attempt_status: enums::AttemptStatus, - apple_pay_flow: Option, + apple_pay_flow: Option, connector: Option, merchant_id: String, ) { if payment_attempt_status == enums::AttemptStatus::Charged { if let Some(flow) = apple_pay_flow { match flow { - enums::ApplePayFlow::Simplified => { + domain::ApplePayFlow::Simplified(_) => { metrics::APPLE_PAY_SIMPLIFIED_FLOW_SUCCESSFUL_PAYMENT.add( &metrics::CONTEXT, 1, @@ -826,7 +826,7 @@ pub fn add_apple_pay_payment_status_metrics( ], ) } - enums::ApplePayFlow::Manual => metrics::APPLE_PAY_MANUAL_FLOW_SUCCESSFUL_PAYMENT + domain::ApplePayFlow::Manual => metrics::APPLE_PAY_MANUAL_FLOW_SUCCESSFUL_PAYMENT .add( &metrics::CONTEXT, 1, @@ -843,7 +843,7 @@ pub fn add_apple_pay_payment_status_metrics( } else if payment_attempt_status == enums::AttemptStatus::Failure { if let Some(flow) = apple_pay_flow { match flow { - enums::ApplePayFlow::Simplified => { + domain::ApplePayFlow::Simplified(_) => { metrics::APPLE_PAY_SIMPLIFIED_FLOW_FAILED_PAYMENT.add( &metrics::CONTEXT, 1, @@ -856,7 +856,7 @@ pub fn add_apple_pay_payment_status_metrics( ], ) } - enums::ApplePayFlow::Manual => metrics::APPLE_PAY_MANUAL_FLOW_FAILED_PAYMENT.add( + domain::ApplePayFlow::Manual => metrics::APPLE_PAY_MANUAL_FLOW_FAILED_PAYMENT.add( &metrics::CONTEXT, 1, &[ diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 10e8b36653..b01c5abb96 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -97,6 +97,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { None, ), connector_meta_data: None, + connector_wallets_details: None, amount_captured: None, access_token: None, session_token: None, @@ -159,6 +160,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { response: Err(types::ErrorResponse::default()), address: PaymentAddress::default(), connector_meta_data: None, + connector_wallets_details: None, amount_captured: None, access_token: None, session_token: None, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index a1f353f4bc..75e30a72d9 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -541,6 +541,7 @@ pub trait ConnectorActions: Connector { connector_meta_data: info .clone() .and_then(|a| a.connector_meta_data.map(Secret::new)), + connector_wallets_details: None, amount_captured: None, access_token: info.clone().and_then(|a| a.access_token), session_token: None, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 110532f524..51f762e371 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -298,6 +298,8 @@ pub enum Flow { GsmRuleRetrieve, /// Gsm Rule Update flow GsmRuleUpdate, + /// Apple pay certificates migration + ApplePayCertificatesMigration, /// Gsm Rule Delete flow GsmRuleDelete, /// User Sign Up diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 4356b6b799..b153c47b88 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -101,6 +101,12 @@ impl From> for StorageError { } } +impl From for StorageError { + fn from(err: diesel::result::Error) -> Self { + Self::from(error_stack::report!(DatabaseError::from(err))) + } +} + impl From> for StorageError { fn from(err: error_stack::Report) -> Self { Self::DatabaseError(err) diff --git a/migrations/2024-05-28-054439_connector_wallets_details/down.sql b/migrations/2024-05-28-054439_connector_wallets_details/down.sql new file mode 100644 index 0000000000..dea26bbd1e --- /dev/null +++ b/migrations/2024-05-28-054439_connector_wallets_details/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_connector_account DROP COLUMN IF EXISTS connector_wallets_details; \ No newline at end of file diff --git a/migrations/2024-05-28-054439_connector_wallets_details/up.sql b/migrations/2024-05-28-054439_connector_wallets_details/up.sql new file mode 100644 index 0000000000..c75de9204d --- /dev/null +++ b/migrations/2024-05-28-054439_connector_wallets_details/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE merchant_connector_account ADD COLUMN IF NOT EXISTS connector_wallets_details BYTEA DEFAULT NULL; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 434e6e2678..c9b0ed7447 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -13813,6 +13813,63 @@ }, "additionalProperties": false }, + "PaymentProcessingDetails": { + "type": "object", + "required": [ + "payment_processing_certificate", + "payment_processing_certificate_key" + ], + "properties": { + "payment_processing_certificate": { + "type": "string" + }, + "payment_processing_certificate_key": { + "type": "string" + } + } + }, + "PaymentProcessingDetailsAt": { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentProcessingDetails" + }, + { + "type": "object", + "required": [ + "payment_processing_details_at" + ], + "properties": { + "payment_processing_details_at": { + "type": "string", + "enum": [ + "Hyperswitch" + ] + } + } + } + ] + }, + { + "type": "object", + "required": [ + "payment_processing_details_at" + ], + "properties": { + "payment_processing_details_at": { + "type": "string", + "enum": [ + "Connector" + ] + } + } + } + ], + "discriminator": { + "propertyName": "payment_processing_details_at" + } + }, "PaymentRetrieveBody": { "type": "object", "properties": { @@ -18544,43 +18601,55 @@ } }, "SessionTokenInfo": { - "type": "object", - "required": [ - "certificate", - "certificate_keys", - "merchant_identifier", - "display_name", - "initiative", - "initiative_context" - ], - "properties": { - "certificate": { - "type": "string" - }, - "certificate_keys": { - "type": "string" - }, - "merchant_identifier": { - "type": "string" - }, - "display_name": { - "type": "string" - }, - "initiative": { - "type": "string" - }, - "initiative_context": { - "type": "string" - }, - "merchant_business_country": { + "allOf": [ + { "allOf": [ { - "$ref": "#/components/schemas/CountryAlpha2" + "$ref": "#/components/schemas/PaymentProcessingDetailsAt" } ], "nullable": true + }, + { + "type": "object", + "required": [ + "certificate", + "certificate_keys", + "merchant_identifier", + "display_name", + "initiative", + "initiative_context" + ], + "properties": { + "certificate": { + "type": "string" + }, + "certificate_keys": { + "type": "string" + }, + "merchant_identifier": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "initiative": { + "type": "string" + }, + "initiative_context": { + "type": "string" + }, + "merchant_business_country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + } + } } - } + ] }, "StraightThroughAlgorithm": { "oneOf": [