diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index d5a4d89910..de85d8c2c0 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1963,6 +1963,9 @@ pub struct ProfileCreate { ///Indicates if clear pan retries is enabled or not. pub is_clear_pan_retries_enabled: Option, + + /// Indicates if 3ds challenge is forced + pub force_3ds_challenge: Option, } #[nutype::nutype( @@ -2234,6 +2237,9 @@ pub struct ProfileResponse { ///Indicates if clear pan retries is enabled or not. pub is_clear_pan_retries_enabled: bool, + + /// Indicates if 3ds challenge is forced + pub force_3ds_challenge: bool, } #[cfg(feature = "v2")] @@ -2501,6 +2507,9 @@ pub struct ProfileUpdate { ///Indicates if clear pan retries is enabled or not. pub is_clear_pan_retries_enabled: Option, + + /// Indicates if 3ds challenge is forced + pub force_3ds_challenge: Option, } #[cfg(feature = "v2")] diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 9a105933e1..11c9b17fae 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -207,6 +207,11 @@ impl SemanticVersion { pub fn get_major(&self) -> u64 { self.0.major } + + /// returns minor version number + pub fn get_minor(&self) -> u64 { + self.0.minor + } /// Constructs new SemanticVersion instance pub fn new(major: u64, minor: u64, patch: u64) -> Self { Self(Version::new(major, minor, patch)) diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 44f32c6eb4..9d47ed7b59 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -64,6 +64,7 @@ pub struct Profile { pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, pub is_clear_pan_retries_enabled: bool, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -113,6 +114,7 @@ pub struct ProfileNew { pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, pub is_clear_pan_retries_enabled: bool, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -160,6 +162,7 @@ pub struct ProfileUpdateInternal { pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, pub is_clear_pan_retries_enabled: Option, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -205,6 +208,7 @@ impl ProfileUpdateInternal { card_testing_guard_config, card_testing_secret_key, is_clear_pan_retries_enabled, + force_3ds_challenge, } = self; Profile { profile_id: source.profile_id, @@ -275,6 +279,7 @@ impl ProfileUpdateInternal { card_testing_secret_key, is_clear_pan_retries_enabled: is_clear_pan_retries_enabled .unwrap_or(source.is_clear_pan_retries_enabled), + force_3ds_challenge, } } } @@ -328,6 +333,7 @@ pub struct Profile { pub card_testing_guard_config: Option, pub card_testing_secret_key: Option, pub is_clear_pan_retries_enabled: bool, + pub force_3ds_challenge: Option, pub routing_algorithm_id: Option, pub order_fulfillment_time: Option, pub order_fulfillment_time_origin: Option, @@ -574,6 +580,7 @@ impl ProfileUpdateInternal { card_testing_secret_key: card_testing_secret_key.or(source.card_testing_secret_key), is_clear_pan_retries_enabled: is_clear_pan_retries_enabled .unwrap_or(source.is_clear_pan_retries_enabled), + force_3ds_challenge: None, } } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 9135baeb77..ce29165ee7 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -220,6 +220,7 @@ diesel::table! { card_testing_guard_config -> Nullable, card_testing_secret_key -> Nullable, is_clear_pan_retries_enabled -> Bool, + force_3ds_challenge -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index af1b2c65f4..36b38f64a7 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -216,6 +216,7 @@ diesel::table! { card_testing_guard_config -> Nullable, card_testing_secret_key -> Nullable, is_clear_pan_retries_enabled -> Bool, + force_3ds_challenge -> Nullable, #[max_length = 64] routing_algorithm_id -> Nullable, order_fulfillment_time -> Nullable, diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs index 054a4930db..fe0e207bc3 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs @@ -368,6 +368,8 @@ pub struct CybersourceConsumerAuthInformation { /// /// For external authentication, this field will always be "Y" veres_enrolled: Option, + /// Raw electronic commerce indicator (ECI) + eci_raw: Option, } #[derive(Debug, Serialize)] @@ -1289,6 +1291,7 @@ impl specification_version: None, pa_specification_version: authn_data.message_version.clone(), veres_enrolled: Some("Y".to_string()), + eci_raw: authn_data.eci.clone(), } }); @@ -1372,6 +1375,7 @@ impl specification_version: None, pa_specification_version: authn_data.message_version.clone(), veres_enrolled: Some("Y".to_string()), + eci_raw: authn_data.eci.clone(), } }); @@ -1453,6 +1457,7 @@ impl specification_version: None, pa_specification_version: authn_data.message_version.clone(), veres_enrolled: Some("Y".to_string()), + eci_raw: authn_data.eci.clone(), } }); @@ -1620,6 +1625,7 @@ impl specification_version: three_ds_info.three_ds_data.specification_version, pa_specification_version: None, veres_enrolled: None, + eci_raw: None, }); let merchant_defined_information = item @@ -1708,6 +1714,7 @@ impl specification_version: None, pa_specification_version: None, veres_enrolled: None, + eci_raw: None, }), merchant_defined_information, }) @@ -1844,6 +1851,7 @@ impl specification_version: None, pa_specification_version: None, veres_enrolled: None, + eci_raw: None, }), merchant_defined_information, }) @@ -2036,6 +2044,7 @@ impl TryFrom<&CybersourceRouterData<&PaymentsAuthorizeRouterData>> for Cybersour specification_version: None, pa_specification_version: None, veres_enrolled: None, + eci_raw: None, }, ), }) diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index d4bb6ed1fc..9e4b79f5d9 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -65,6 +65,7 @@ pub struct Profile { pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, pub is_clear_pan_retries_enabled: bool, + pub force_3ds_challenge: bool, } #[cfg(feature = "v1")] @@ -112,6 +113,7 @@ pub struct ProfileSetter { pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, pub is_clear_pan_retries_enabled: bool, + pub force_3ds_challenge: bool, } #[cfg(feature = "v1")] @@ -165,6 +167,7 @@ impl From for Profile { card_testing_guard_config: value.card_testing_guard_config, card_testing_secret_key: value.card_testing_secret_key, is_clear_pan_retries_enabled: value.is_clear_pan_retries_enabled, + force_3ds_challenge: value.force_3ds_challenge, } } } @@ -220,6 +223,7 @@ pub struct ProfileGeneralUpdate { pub card_testing_guard_config: Option, pub card_testing_secret_key: OptionalEncryptableName, pub is_clear_pan_retries_enabled: Option, + pub force_3ds_challenge: bool, } #[cfg(feature = "v1")] @@ -290,6 +294,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config, card_testing_secret_key, is_clear_pan_retries_enabled, + force_3ds_challenge, } = *update; Self { @@ -333,6 +338,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config, card_testing_secret_key: card_testing_secret_key.map(Encryption::from), is_clear_pan_retries_enabled, + force_3ds_challenge: Some(force_3ds_challenge), } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -378,6 +384,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config: None, card_testing_secret_key: None, is_clear_pan_retries_enabled: None, + force_3ds_challenge: None, }, ProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -421,6 +428,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config: None, card_testing_secret_key: None, is_clear_pan_retries_enabled: None, + force_3ds_challenge: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -464,6 +472,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config: None, card_testing_secret_key: None, is_clear_pan_retries_enabled: None, + force_3ds_challenge: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -507,6 +516,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config: None, card_testing_secret_key: None, is_clear_pan_retries_enabled: None, + force_3ds_challenge: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -550,6 +560,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config: None, card_testing_secret_key: None, is_clear_pan_retries_enabled: None, + force_3ds_challenge: None, }, ProfileUpdate::CardTestingSecretKeyUpdate { card_testing_secret_key, @@ -593,6 +604,7 @@ impl From for ProfileUpdateInternal { card_testing_guard_config: None, card_testing_secret_key: card_testing_secret_key.map(Encryption::from), is_clear_pan_retries_enabled: None, + force_3ds_challenge: None, }, } } @@ -655,6 +667,7 @@ impl super::behaviour::Conversion for Profile { card_testing_guard_config: self.card_testing_guard_config, card_testing_secret_key: self.card_testing_secret_key.map(|name| name.into()), is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, + force_3ds_challenge: Some(self.force_3ds_challenge), }) } @@ -742,6 +755,7 @@ impl super::behaviour::Conversion for Profile { }) .await?, is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled, + force_3ds_challenge: item.force_3ds_challenge.unwrap_or_default(), }) } .await @@ -799,6 +813,7 @@ impl super::behaviour::Conversion for Profile { card_testing_guard_config: self.card_testing_guard_config, card_testing_secret_key: self.card_testing_secret_key.map(Encryption::from), is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, + force_3ds_challenge: Some(self.force_3ds_challenge), }) } } @@ -1547,6 +1562,7 @@ impl super::behaviour::Conversion for Profile { card_testing_guard_config: self.card_testing_guard_config, card_testing_secret_key: self.card_testing_secret_key.map(|name| name.into()), is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, + force_3ds_challenge: None, }) } diff --git a/crates/hyperswitch_domain_models/src/router_request_types/authentication.rs b/crates/hyperswitch_domain_models/src/router_request_types/authentication.rs index 06d73644e3..74bdb94dd7 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types/authentication.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types/authentication.rs @@ -92,6 +92,7 @@ pub struct ConnectorAuthenticationRequestData { pub threeds_method_comp_ind: api_models::payments::ThreeDsCompletionIndicator, pub three_ds_requestor_url: String, pub webhook_url: String, + pub force_3ds_challenge: bool, } #[derive(Clone, serde::Deserialize, Debug, serde::Serialize, PartialEq, Eq)] diff --git a/crates/router/src/connector/netcetera/netcetera_types.rs b/crates/router/src/connector/netcetera/netcetera_types.rs index 8f4a596131..4869c228b4 100644 --- a/crates/router/src/connector/netcetera/netcetera_types.rs +++ b/crates/router/src/connector/netcetera/netcetera_types.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use common_utils::pii::Email; +use common_utils::{pii::Email, types::SemanticVersion}; use hyperswitch_connectors::utils::AddressDetailsData; use masking::ExposeInterface; use serde::{Deserialize, Serialize}; @@ -15,6 +15,16 @@ pub enum SingleOrListElement { List(Vec), } +impl SingleOrListElement { + fn get_version_checked(message_version: SemanticVersion, value: T) -> Self { + if message_version.get_major() >= 2 && message_version.get_minor() >= 3 { + Self::List(vec![value]) + } else { + Self::Single(value) + } + } +} + impl SingleOrListElement { pub fn new_single(value: T) -> Self { Self::Single(value) @@ -121,7 +131,7 @@ pub struct ThreeDSRequestor { /// This field is required when deviceChannel = 01 (APP) and unless market or regional mandate restricts sending /// this information. /// Available for supporting EMV 3DS 2.3.1 and later versions. - pub app_ip: Option, + pub app_ip: Option, /// Indicate if the 3DS Requestor supports the SPC authentication. /// /// The accepted values are: @@ -169,6 +179,44 @@ pub enum ThreeDSRequestorAuthenticationIndicator { BillingAgreement, } +impl ThreeDSRequestor { + pub fn new( + app_ip: Option, + psd2_sca_exemption_type: Option, + force_3ds_challenge: bool, + message_version: SemanticVersion, + ) -> Self { + // if sca exemption is provided, we need to set the challenge indicator to NoChallengeRequestedTransactionalRiskAnalysis + let three_ds_requestor_challenge_ind = if force_3ds_challenge { + Some(SingleOrListElement::get_version_checked( + message_version, + ThreeDSRequestorChallengeIndicator::ChallengeRequestedMandate, + )) + } else if let Some(common_enums::ScaExemptionType::TransactionRiskAnalysis) = + psd2_sca_exemption_type + { + Some(SingleOrListElement::get_version_checked( + message_version, + ThreeDSRequestorChallengeIndicator::NoChallengeRequestedTransactionalRiskAnalysis, + )) + } else { + None + }; + + Self { + three_ds_requestor_authentication_ind: ThreeDSRequestorAuthenticationIndicator::Payment, + three_ds_requestor_authentication_info: None, + three_ds_requestor_challenge_ind, + three_ds_requestor_prior_authentication_info: None, + three_ds_requestor_dec_req_ind: None, + three_ds_requestor_dec_max_time: None, + app_ip, + three_ds_requestor_spc_support: None, + spc_incomp_ind: None, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ThreeDSRequestorAuthenticationInformation { @@ -303,6 +351,39 @@ pub enum ThreeDSRequestorDecoupledRequestIndicator { B, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum SchemeId { + Visa, + Mastercard, + #[serde(rename = "JCB")] + Jcb, + #[serde(rename = "American Express")] + AmericanExpress, + Diners, + // For Cartes Bancaires and UnionPay, it is recommended to send the scheme ID + #[serde(rename = "CB")] + CartesBancaires, + UnionPay, +} + +impl TryFrom for SchemeId { + type Error = error_stack::Report; + fn try_from(network: common_enums::CardNetwork) -> Result { + match network { + common_enums::CardNetwork::Visa => Ok(Self::Visa), + common_enums::CardNetwork::Mastercard => Ok(Self::Mastercard), + common_enums::CardNetwork::JCB => Ok(Self::Jcb), + common_enums::CardNetwork::AmericanExpress => Ok(Self::AmericanExpress), + common_enums::CardNetwork::DinersClub => Ok(Self::Diners), + common_enums::CardNetwork::CartesBancaires => Ok(Self::CartesBancaires), + common_enums::CardNetwork::UnionPay => Ok(Self::UnionPay), + _ => Err(errors::ConnectorError::RequestEncodingFailedWithReason( + "Invalid card network".to_string(), + ))?, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct CardholderAccount { @@ -337,7 +418,7 @@ pub struct CardholderAccount { /// are provided in the 3DS Server Configuration Properties. Additionally, /// if the schemeId is present in the request and there are card ranges found by multiple schemes, the schemeId will be /// used for proper resolving of the versioning data. - pub scheme_id: Option, + pub scheme_id: Option, /// Additional information about the account optionally provided by the 3DS Requestor. /// /// This field is limited to 64 characters and it is optional to use. @@ -1048,6 +1129,7 @@ pub struct AcquirerData { /// This field is required if no MerchantAcquirer is present for the acquirer BIN in the 3DS Server configuration and /// for requests where messageCategory = 01 (PA). For requests where messageCategory=02 (NPA), the field is required /// only if scheme is Mastercard, for other schemes it is optional. + #[serde(skip_serializing_if = "Option::is_none")] pub acquirer_bin: Option, /// Acquirer-assigned Merchant identifier. @@ -1061,6 +1143,7 @@ pub struct AcquirerData { /// /// This field is required if merchantConfigurationId is not provided in the request and messageCategory = 01 (PA). /// For Mastercard, if merchantConfigurationId is not provided, the field must be present if messageCategory = 02 (NPA). + #[serde(skip_serializing_if = "Option::is_none")] pub acquirer_merchant_id: Option, /// Acquirer Country Code. @@ -1072,6 +1155,7 @@ pub struct AcquirerData { /// The Directory Server may edit the value of this field provided by the 3DS Server. /// /// This field is required. + #[serde(skip_serializing_if = "Option::is_none")] pub acquirer_country_code: Option, } @@ -1091,6 +1175,7 @@ pub struct MerchantData { /// If not present in the request it will be filled from the merchant configuration referenced by the merchantConfigurationId. /// /// This field is required for messageCategory=01 (PA) and optional, but strongly recommended for 02 (NPA). + #[serde(skip_serializing_if = "Option::is_none")] pub mcc: Option, /// Country code for the merchant. This value correlates to the Merchant Country Code as defined by each Payment System or DS. @@ -1099,6 +1184,7 @@ pub struct MerchantData { /// If not present in the request it will be filled from the merchant configuration referenced by the merchantConfigurationId. /// /// This field is required for messageCategory=01 (PA) and optional, but strongly recommended for 02 (NPA). + #[serde(skip_serializing_if = "Option::is_none")] pub merchant_country_code: Option, /// Merchant name assigned by the Acquirer or Payment System. This field is limited to maximum 40 characters, @@ -1107,6 +1193,7 @@ pub struct MerchantData { /// If not present in the request it will be filled from the merchant configuration referenced by the merchantConfigurationId. /// /// This field is required for messageCategory=01 (PA) and optional, but strongly recommended for 02 (NPA). + #[serde(skip_serializing_if = "Option::is_none")] pub merchant_name: Option, /// Fully qualified URL of the merchant that receives the CRes message or Error Message. @@ -1116,6 +1203,7 @@ pub struct MerchantData { /// This field should be present if the merchant will receive the final CRes message and the device channel is BROWSER. /// If not present in the request it will be filled from the notificationURL configured in the XML or database configuration. #[serde(rename = "notificationURL")] + #[serde(skip_serializing_if = "Option::is_none")] pub notification_url: Option, /// Each DS provides rules for the 3DS Requestor ID. The 3DS Requestor is responsible for providing the 3DS Requestor ID according to the DS rules. @@ -1123,6 +1211,7 @@ pub struct MerchantData { /// This value is mandatory, therefore it should be either configured for each Merchant Acquirer, or should be /// passed in the transaction payload as part of the Merchant data. #[serde(rename = "threeDSRequestorId")] + #[serde(skip_serializing_if = "Option::is_none")] pub three_ds_requestor_id: Option, /// Each DS provides rules for the 3DS Requestor Name. The 3DS Requestor is responsible for providing the 3DS Requestor Name according to the DS rules. @@ -1130,6 +1219,7 @@ pub struct MerchantData { /// This value is mandatory, therefore it should be either configured for each Merchant Acquirer, or should be /// passed in the transaction payload as part of the Merchant data. #[serde(rename = "threeDSRequestorName")] + #[serde(skip_serializing_if = "Option::is_none")] pub three_ds_requestor_name: Option, /// Set whitelisting status of the merchant. @@ -1160,6 +1250,7 @@ pub struct MerchantData { /// /// If not present in the request it will be filled from the notificationURL configured in the XML or database /// configuration. + #[serde(skip_serializing_if = "Option::is_none")] pub results_response_notification_url: Option, } @@ -1403,32 +1494,6 @@ impl From for Browser { } } -impl From> for ThreeDSRequestor { - fn from(value: Option) -> Self { - // if sca exemption is provided, we need to set the challenge indicator to NoChallengeRequestedTransactionalRiskAnalysis - let three_ds_requestor_challenge_ind = - if let Some(common_enums::ScaExemptionType::TransactionRiskAnalysis) = value { - Some(SingleOrListElement::Single( - ThreeDSRequestorChallengeIndicator::NoChallengeRequestedTransactionalRiskAnalysis, - )) - } else { - None - }; - - Self { - three_ds_requestor_authentication_ind: ThreeDSRequestorAuthenticationIndicator::Payment, - three_ds_requestor_authentication_info: None, - three_ds_requestor_challenge_ind, - three_ds_requestor_prior_authentication_info: None, - three_ds_requestor_dec_req_ind: None, - three_ds_requestor_dec_max_time: None, - app_ip: None, - three_ds_requestor_spc_support: None, - spc_incomp_ind: None, - } - } -} - #[derive(Serialize, Deserialize, Debug, Clone)] pub enum ChallengeWindowSizeEnum { #[serde(rename = "01")] diff --git a/crates/router/src/connector/netcetera/transformers.rs b/crates/router/src/connector/netcetera/transformers.rs index 213433972c..886dbc18ad 100644 --- a/crates/router/src/connector/netcetera/transformers.rs +++ b/crates/router/src/connector/netcetera/transformers.rs @@ -279,22 +279,7 @@ impl TryFrom<&Option> for NetceteraMetaData #[serde(rename_all = "camelCase")] pub struct NetceteraPreAuthenticationRequest { cardholder_account_number: cards::CardNumber, - scheme_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum SchemeId { - Visa, - Mastercard, - #[serde(rename = "JCB")] - Jcb, - #[serde(rename = "American Express")] - AmericanExpress, - Diners, - // For Cartes Bancaires and UnionPay, it is recommended to send the scheme ID - #[serde(rename = "CB")] - CartesBancaires, - UnionPay, + scheme_id: Option, } #[derive(Debug, Deserialize, Serialize)] @@ -325,7 +310,7 @@ impl NetceteraPreAuthenticationResponseData { #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct CardRange { - pub scheme_id: SchemeId, + pub scheme_id: netcetera_types::SchemeId, pub directory_server_id: Option, pub acs_protocol_versions: Vec, #[serde(rename = "threeDSMethodDataForm")] @@ -387,7 +372,8 @@ impl TryFrom<&NetceteraRouterData<&types::authentication::PreAuthNRouterData>> .clone() .map(|card_network| { is_cobadged_card().map(|is_cobadged_card| { - is_cobadged_card.then_some(SchemeId::try_from(card_network)) + is_cobadged_card + .then_some(netcetera_types::SchemeId::try_from(card_network)) }) }) .transpose()? @@ -397,24 +383,6 @@ impl TryFrom<&NetceteraRouterData<&types::authentication::PreAuthNRouterData>> } } -impl TryFrom for SchemeId { - type Error = error_stack::Report; - fn try_from(network: common_enums::CardNetwork) -> Result { - match network { - common_enums::CardNetwork::Visa => Ok(Self::Visa), - common_enums::CardNetwork::Mastercard => Ok(Self::Mastercard), - common_enums::CardNetwork::JCB => Ok(Self::Jcb), - common_enums::CardNetwork::AmericanExpress => Ok(Self::AmericanExpress), - common_enums::CardNetwork::DinersClub => Ok(Self::Diners), - common_enums::CardNetwork::CartesBancaires => Ok(Self::CartesBancaires), - common_enums::CardNetwork::UnionPay => Ok(Self::UnionPay), - _ => Err(errors::ConnectorError::RequestEncodingFailedWithReason( - "Invalid card network".to_string(), - ))?, - } - } -} - #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] #[serde_with::skip_serializing_none] @@ -496,15 +464,39 @@ impl TryFrom<&NetceteraRouterData<&types::authentication::ConnectorAuthenticatio let now = common_utils::date_time::now(); let request = item.router_data.request.clone(); let pre_authn_data = request.pre_authentication_data.clone(); - let three_ds_requestor = - netcetera_types::ThreeDSRequestor::from(item.router_data.psd2_sca_exemption_type); + let ip_address = request + .browser_details + .as_ref() + .and_then(|browser| browser.ip_address); + let three_ds_requestor = netcetera_types::ThreeDSRequestor::new( + ip_address, + item.router_data.psd2_sca_exemption_type, + item.router_data.request.force_3ds_challenge, + item.router_data + .request + .pre_authentication_data + .message_version + .clone(), + ); let card = utils::get_card_details(request.payment_method_data, "netcetera")?; + let is_cobadged_card = card + .card_number + .clone() + .is_cobadged_card() + .change_context(errors::ConnectorError::RequestEncodingFailed) + .attach_printable("error while checking is_cobadged_card")?; let cardholder_account = netcetera_types::CardholderAccount { acct_type: None, card_expiry_date: Some(card.get_expiry_date_as_yymm()?), acct_info: None, acct_number: card.card_number, - scheme_id: None, + scheme_id: card + .card_network + .clone() + .and_then(|card_network| { + is_cobadged_card.then_some(netcetera_types::SchemeId::try_from(card_network)) + }) + .transpose()?, acct_id: None, pay_token_ind: None, pay_token_info: None, @@ -535,7 +527,8 @@ impl TryFrom<&NetceteraRouterData<&types::authentication::ConnectorAuthenticatio ), recurring_expiry: None, recurring_frequency: None, - trans_type: None, + // 01 -> Goods and Services, hardcoding this as we serve this usecase only for now + trans_type: Some("01".to_string()), recurring_amount: None, recurring_currency: None, recurring_exponent: None, @@ -597,7 +590,11 @@ impl TryFrom<&NetceteraRouterData<&types::authentication::ConnectorAuthenticatio }; Ok(Self { preferred_protocol_version: Some(pre_authn_data.message_version), - enforce_preferred_protocol_version: None, + // For Device channel App, we should enforce the preferred protocol version + enforce_preferred_protocol_version: Some(matches!( + request.device_channel, + api_models::payments::DeviceChannel::App + )), device_channel: netcetera_types::NetceteraDeviceChannel::from(request.device_channel), message_category: netcetera_types::NetceteraMessageCategory::from( request.message_category, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index d71985a596..4d4ff40666 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3787,6 +3787,7 @@ impl ProfileCreateBridge for api::ProfileCreate { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error while generating card testing secret key")?, is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled.unwrap_or_default(), + force_3ds_challenge: self.force_3ds_challenge.unwrap_or_default(), })) } @@ -4228,6 +4229,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { .map(ForeignInto::foreign_into), card_testing_secret_key, is_clear_pan_retries_enabled: self.is_clear_pan_retries_enabled, + force_3ds_challenge: self.force_3ds_challenge.unwrap_or_default(), }, ))) } diff --git a/crates/router/src/core/authentication.rs b/crates/router/src/core/authentication.rs index 7a91087ce3..28d88ac1fc 100644 --- a/crates/router/src/core/authentication.rs +++ b/crates/router/src/core/authentication.rs @@ -41,6 +41,7 @@ pub async fn perform_authentication( three_ds_requestor_url: String, psd2_sca_exemption_type: Option, payment_id: common_utils::id_type::PaymentId, + force_3ds_challenge: bool, ) -> CustomResult { let router_data = transformers::construct_authentication_router_data( state, @@ -65,6 +66,7 @@ pub async fn perform_authentication( three_ds_requestor_url, psd2_sca_exemption_type, payment_id, + force_3ds_challenge, )?; let response = Box::pin(utils::do_auth_connector_call( state, diff --git a/crates/router/src/core/authentication/transformers.rs b/crates/router/src/core/authentication/transformers.rs index 97657e767d..ab28addb01 100644 --- a/crates/router/src/core/authentication/transformers.rs +++ b/crates/router/src/core/authentication/transformers.rs @@ -47,6 +47,7 @@ pub fn construct_authentication_router_data( three_ds_requestor_url: String, psd2_sca_exemption_type: Option, payment_id: common_utils::id_type::PaymentId, + force_3ds_challenge: bool, ) -> RouterResult { let router_request = types::authentication::ConnectorAuthenticationRequestData { payment_method_data, @@ -66,6 +67,7 @@ pub fn construct_authentication_router_data( three_ds_requestor_url, threeds_method_comp_ind, webhook_url, + force_3ds_challenge, }; construct_router_data( state, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 21d208f432..03126ea771 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -7609,6 +7609,7 @@ pub async fn payment_external_authentication( authentication_details.three_ds_requestor_url.clone(), payment_intent.psd2_sca_exemption_type, payment_intent.payment_id, + business_profile.force_3ds_challenge, )) .await? }; diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 8c720875f1..80d5e300be 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -188,6 +188,7 @@ impl ForeignTryFrom for ProfileResponse { .card_testing_guard_config .map(ForeignInto::foreign_into), is_clear_pan_retries_enabled: item.is_clear_pan_retries_enabled, + force_3ds_challenge: item.force_3ds_challenge, }) } } @@ -438,5 +439,6 @@ pub async fn create_profile_from_merchant_account( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error while generating card testing secret key")?, is_clear_pan_retries_enabled: request.is_clear_pan_retries_enabled.unwrap_or_default(), + force_3ds_challenge: request.force_3ds_challenge.unwrap_or_default(), })) } diff --git a/migrations/2025-03-04-105454_add_force_3ds_challenge_column_to_business_profile/down.sql b/migrations/2025-03-04-105454_add_force_3ds_challenge_column_to_business_profile/down.sql new file mode 100644 index 0000000000..8642c8ae52 --- /dev/null +++ b/migrations/2025-03-04-105454_add_force_3ds_challenge_column_to_business_profile/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile +DROP COLUMN force_3ds_challenge; \ No newline at end of file diff --git a/migrations/2025-03-04-105454_add_force_3ds_challenge_column_to_business_profile/up.sql b/migrations/2025-03-04-105454_add_force_3ds_challenge_column_to_business_profile/up.sql new file mode 100644 index 0000000000..340d638b2b --- /dev/null +++ b/migrations/2025-03-04-105454_add_force_3ds_challenge_column_to_business_profile/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE business_profile +ADD COLUMN IF NOT EXISTS force_3ds_challenge boolean DEFAULT false; \ No newline at end of file