diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 49f6d15a6d..687e42728f 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -190,6 +190,30 @@ impl AttemptStatus { } } +#[derive( + Clone, + Copy, + Debug, + Hash, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + strum::EnumIter, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ApplePayPaymentMethodType { + Debit, + Credit, + Prepaid, + Store, +} + /// Indicates the method by which a card is discovered during a payment #[derive( Clone, diff --git a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs index bafbc7920a..d972c958b7 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs @@ -2218,7 +2218,7 @@ impl TryFrom<(&WalletData, &PaymentsAuthorizeRouterData)> for AdyenPaymentMethod number: apple_pay_decrypte.application_primary_account_number, expiry_month: exp_month, expiry_year: expiry_year_4_digit, - brand: "applepay".to_string(), + brand: data.payment_method.network.clone(), payment_type: PaymentType::Scheme, }; Ok(AdyenPaymentMethod::ApplePayDecrypt(Box::new( diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 167cc7a2f0..f2f8647936 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -10,6 +10,7 @@ use common_enums::enums as api_enums; #[cfg(feature = "v2")] use common_utils::ext_traits::OptionExt; use common_utils::{ + ext_traits::StringExt, id_type, new_type::{ MaskedBankAccount, MaskedIban, MaskedRoutingNumber, MaskedSortCode, MaskedUpiVpaId, @@ -466,6 +467,26 @@ pub struct ApplePayWalletData { pub transaction_identifier: String, } +impl ApplePayWalletData { + pub fn get_payment_method_type(&self) -> Option { + self.payment_method + .pm_type + .clone() + .parse_enum("ApplePayPaymentMethodType") + .ok() + .and_then(|payment_type| match payment_type { + common_enums::ApplePayPaymentMethodType::Debit => { + Some(api_enums::PaymentMethodType::Debit) + } + common_enums::ApplePayPaymentMethodType::Credit => { + Some(api_enums::PaymentMethodType::Credit) + } + common_enums::ApplePayPaymentMethodType::Prepaid + | common_enums::ApplePayPaymentMethodType::Store => None, + }) + } +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct ApplepayPaymentMethod { pub display_name: String, diff --git a/crates/router/src/core/debit_routing.rs b/crates/router/src/core/debit_routing.rs index 651f1ea04c..0aeb2b9777 100644 --- a/crates/router/src/core/debit_routing.rs +++ b/crates/router/src/core/debit_routing.rs @@ -6,7 +6,7 @@ use common_utils::{ errors::CustomResult, ext_traits::ValueExt, id_type, types::keymanager::KeyManagerState, }; use error_stack::ResultExt; -use masking::Secret; +use masking::{PeekInterface, Secret}; use super::{ payments::{OperationSessionGetters, OperationSessionSetters}, @@ -157,33 +157,76 @@ where match format!("{operation:?}").as_str() { "PaymentConfirm" => { logger::info!("Checking if debit routing is required"); - let payment_intent = payment_data.get_payment_intent(); - let payment_attempt = payment_data.get_payment_attempt(); - request_validation(payment_intent, payment_attempt, debit_routing_config) + request_validation(payment_data, debit_routing_config) } _ => false, } } -pub fn request_validation( - payment_intent: &hyperswitch_domain_models::payments::PaymentIntent, - payment_attempt: &hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt, +fn request_validation( + payment_data: &D, debit_routing_config: &settings::DebitRoutingConfig, -) -> bool { - logger::debug!("Validating request for debit routing"); - let is_currency_supported = payment_intent.currency.map(|currency| { - debit_routing_config - .supported_currencies - .contains(¤cy) - }); +) -> bool +where + D: OperationSessionGetters + Send + Sync + Clone, +{ + let payment_intent = payment_data.get_payment_intent(); + let payment_attempt = payment_data.get_payment_attempt(); + + let is_currency_supported = is_currency_supported(payment_intent, debit_routing_config); + + let is_valid_payment_method = validate_payment_method_for_debit_routing(payment_data); payment_intent.setup_future_usage != Some(enums::FutureUsage::OffSession) && payment_intent.amount.is_greater_than(0) - && is_currency_supported == Some(true) - && payment_attempt.authentication_type != Some(enums::AuthenticationType::ThreeDs) - && payment_attempt.payment_method == Some(enums::PaymentMethod::Card) - && payment_attempt.payment_method_type == Some(enums::PaymentMethodType::Debit) + && is_currency_supported + && payment_attempt.authentication_type == Some(enums::AuthenticationType::NoThreeDs) + && is_valid_payment_method +} + +fn is_currency_supported( + payment_intent: &hyperswitch_domain_models::payments::PaymentIntent, + debit_routing_config: &settings::DebitRoutingConfig, +) -> bool { + payment_intent + .currency + .map(|currency| { + debit_routing_config + .supported_currencies + .contains(¤cy) + }) + .unwrap_or(false) +} + +fn validate_payment_method_for_debit_routing(payment_data: &D) -> bool +where + D: OperationSessionGetters + Send + Sync + Clone, +{ + let payment_attempt = payment_data.get_payment_attempt(); + match payment_attempt.payment_method { + Some(enums::PaymentMethod::Card) => { + payment_attempt.payment_method_type == Some(enums::PaymentMethodType::Debit) + } + Some(enums::PaymentMethod::Wallet) => { + payment_attempt.payment_method_type == Some(enums::PaymentMethodType::ApplePay) + && payment_data + .get_payment_method_data() + .and_then(|data| data.get_wallet_data()) + .and_then(|data| data.get_apple_pay_wallet_data()) + .and_then(|data| data.get_payment_method_type()) + == Some(enums::PaymentMethodType::Debit) + && matches!( + payment_data.get_payment_method_token().cloned(), + Some( + hyperswitch_domain_models::router_data::PaymentMethodToken::ApplePayDecrypt( + _ + ) + ) + ) + } + _ => false, + } } pub async fn check_for_debit_routing_connector_in_profile< @@ -318,8 +361,11 @@ pub async fn get_debit_routing_output< ) -> Option { logger::debug!("Fetching sorted card networks"); - let (saved_co_badged_card_data, saved_card_type, card_isin) = - extract_saved_card_info(payment_data); + let card_info = extract_card_info(payment_data); + + let saved_co_badged_card_data = card_info.co_badged_card_data; + let saved_card_type = card_info.card_type; + let card_isin = card_info.card_isin; match ( saved_co_badged_card_data @@ -370,46 +416,131 @@ pub async fn get_debit_routing_output< } } -fn extract_saved_card_info( - payment_data: &D, -) -> ( - Option, - Option, - Option>, -) +#[derive(Debug, Clone)] +struct ExtractedCardInfo { + co_badged_card_data: Option, + card_type: Option, + card_isin: Option>, +} + +impl ExtractedCardInfo { + fn new( + co_badged_card_data: Option, + card_type: Option, + card_isin: Option>, + ) -> Self { + Self { + co_badged_card_data, + card_type, + card_isin, + } + } + + fn empty() -> Self { + Self::new(None, None, None) + } +} + +fn extract_card_info(payment_data: &D) -> ExtractedCardInfo where D: OperationSessionGetters, { - let payment_method_data_optional = payment_data.get_payment_method_data(); - match payment_data - .get_payment_method_info() - .and_then(|info| info.get_payment_methods_data()) + extract_from_saved_payment_method(payment_data) + .unwrap_or_else(|| extract_from_payment_method_data(payment_data)) +} + +fn extract_from_saved_payment_method(payment_data: &D) -> Option +where + D: OperationSessionGetters, +{ + let payment_methods_data = payment_data + .get_payment_method_info()? + .get_payment_methods_data()?; + + if let hyperswitch_domain_models::payment_method_data::PaymentMethodsData::Card(card) = + payment_methods_data { - Some(hyperswitch_domain_models::payment_method_data::PaymentMethodsData::Card(card)) => { - match (&card.co_badged_card_data, &card.card_isin) { - (Some(co_badged), _) => { - logger::debug!("Co-badged card data found in saved payment method"); - (Some(co_badged.clone()), card.card_type, None) - } - (None, Some(card_isin)) => { - logger::debug!("No co-badged data; using saved card ISIN"); - (None, None, Some(Secret::new(card_isin.clone()))) - } - _ => (None, None, None), - } - } - _ => match payment_method_data_optional { - Some(hyperswitch_domain_models::payment_method_data::PaymentMethodData::Card(card)) => { - logger::debug!("Using card data from payment request"); - ( - None, - None, - Some(Secret::new(card.card_number.get_card_isin())), - ) - } - _ => (None, None, None), - }, + return Some(extract_card_info_from_saved_card(&card)); } + + None +} + +fn extract_card_info_from_saved_card( + card: &hyperswitch_domain_models::payment_method_data::CardDetailsPaymentMethod, +) -> ExtractedCardInfo { + match (&card.co_badged_card_data, &card.card_isin) { + (Some(co_badged), _) => { + logger::debug!("Co-badged card data found in saved payment method"); + ExtractedCardInfo::new(Some(co_badged.clone()), card.card_type.clone(), None) + } + (None, Some(card_isin)) => { + logger::debug!("No co-badged data; using saved card ISIN"); + ExtractedCardInfo::new(None, None, Some(Secret::new(card_isin.clone()))) + } + _ => ExtractedCardInfo::empty(), + } +} + +fn extract_from_payment_method_data(payment_data: &D) -> ExtractedCardInfo +where + D: OperationSessionGetters, +{ + match payment_data.get_payment_method_data() { + Some(hyperswitch_domain_models::payment_method_data::PaymentMethodData::Card(card)) => { + logger::debug!("Using card data from payment request"); + ExtractedCardInfo::new( + None, + None, + Some(Secret::new(card.card_number.get_extended_card_bin())), + ) + } + Some(hyperswitch_domain_models::payment_method_data::PaymentMethodData::Wallet( + wallet_data, + )) => extract_from_wallet_data(wallet_data, payment_data), + _ => ExtractedCardInfo::empty(), + } +} + +fn extract_from_wallet_data( + wallet_data: &hyperswitch_domain_models::payment_method_data::WalletData, + payment_data: &D, +) -> ExtractedCardInfo +where + D: OperationSessionGetters, +{ + match wallet_data { + hyperswitch_domain_models::payment_method_data::WalletData::ApplePay(_) => { + logger::debug!("Using Apple Pay data from payment request"); + let apple_pay_isin = extract_apple_pay_isin(payment_data); + ExtractedCardInfo::new(None, None, apple_pay_isin) + } + _ => ExtractedCardInfo::empty(), + } +} + +fn extract_apple_pay_isin(payment_data: &D) -> Option> +where + D: OperationSessionGetters, +{ + payment_data.get_payment_method_token().and_then(|token| { + if let hyperswitch_domain_models::router_data::PaymentMethodToken::ApplePayDecrypt( + apple_pay_decrypt_data, + ) = token + { + logger::debug!("Using Apple Pay decrypt data from payment method token"); + Some(Secret::new( + apple_pay_decrypt_data + .application_primary_account_number + .peek() + .chars() + .take(8) + .collect::(), + )) + } else { + None + } + }) } async fn handle_retryable_connector( diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e5772c323d..a8f718e239 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -9916,10 +9916,24 @@ impl OperationSessionSetters for PaymentData { } fn set_card_network(&mut self, card_network: enums::CardNetwork) { - if let Some(domain::PaymentMethodData::Card(card)) = &mut self.payment_method_data { - logger::debug!("set card network {:?}", card_network.clone()); - card.card_network = Some(card_network); - }; + match &mut self.payment_method_data { + Some(domain::PaymentMethodData::Card(card)) => { + logger::debug!("Setting card network: {:?}", card_network); + card.card_network = Some(card_network); + } + Some(domain::PaymentMethodData::Wallet(wallet_data)) => match wallet_data { + hyperswitch_domain_models::payment_method_data::WalletData::ApplePay(wallet) => { + logger::debug!("Setting Apple Pay card network: {:?}", card_network); + wallet.payment_method.network = card_network.to_string(); + } + _ => { + logger::debug!("Wallet type does not support setting card network."); + } + }, + _ => { + logger::warn!("Payment method data does not support setting card network."); + } + } } fn set_co_badged_card_data(