feat(connector): add support for external authentication for cybersource (#4714)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Hrithikesh
2024-05-29 19:08:06 +05:30
committed by GitHub
parent 0f7f3d9e74
commit 97f2ff0e70
16 changed files with 144 additions and 10 deletions

View File

@ -244,10 +244,9 @@ impl Connector {
| Self::Riskified | Self::Riskified
| Self::Threedsecureio | Self::Threedsecureio
| Self::Netcetera | Self::Netcetera
| Self::Cybersource
| Self::Noon | Self::Noon
| Self::Stripe => false, | Self::Stripe => false,
Self::Checkout | Self::Nmi => true, Self::Checkout | Self::Nmi| Self::Cybersource => true,
} }
} }
} }

View File

@ -42,6 +42,7 @@ pub struct Authentication {
pub profile_id: String, pub profile_id: String,
pub payment_id: Option<String>, pub payment_id: Option<String>,
pub merchant_connector_id: String, pub merchant_connector_id: String,
pub ds_trans_id: Option<String>,
pub directory_server_id: Option<String>, pub directory_server_id: Option<String>,
} }
@ -87,6 +88,7 @@ pub struct AuthenticationNew {
pub profile_id: String, pub profile_id: String,
pub payment_id: Option<String>, pub payment_id: Option<String>,
pub merchant_connector_id: String, pub merchant_connector_id: String,
pub ds_trans_id: Option<String>,
pub directory_server_id: Option<String>, pub directory_server_id: Option<String>,
} }
@ -115,6 +117,7 @@ pub enum AuthenticationUpdate {
acs_trans_id: Option<String>, acs_trans_id: Option<String>,
acs_signed_content: Option<String>, acs_signed_content: Option<String>,
authentication_status: common_enums::AuthenticationStatus, authentication_status: common_enums::AuthenticationStatus,
ds_trans_id: Option<String>,
}, },
PostAuthenticationUpdate { PostAuthenticationUpdate {
trans_status: common_enums::TransactionStatus, trans_status: common_enums::TransactionStatus,
@ -162,6 +165,7 @@ pub struct AuthenticationUpdateInternal {
pub acs_reference_number: Option<String>, pub acs_reference_number: Option<String>,
pub acs_trans_id: Option<String>, pub acs_trans_id: Option<String>,
pub acs_signed_content: Option<String>, pub acs_signed_content: Option<String>,
pub ds_trans_id: Option<String>,
pub directory_server_id: Option<String>, pub directory_server_id: Option<String>,
} }
@ -193,6 +197,7 @@ impl Default for AuthenticationUpdateInternal {
acs_reference_number: Default::default(), acs_reference_number: Default::default(),
acs_trans_id: Default::default(), acs_trans_id: Default::default(),
acs_signed_content: Default::default(), acs_signed_content: Default::default(),
ds_trans_id: Default::default(),
directory_server_id: Default::default(), directory_server_id: Default::default(),
} }
} }
@ -226,6 +231,7 @@ impl AuthenticationUpdateInternal {
acs_reference_number, acs_reference_number,
acs_trans_id, acs_trans_id,
acs_signed_content, acs_signed_content,
ds_trans_id,
directory_server_id, directory_server_id,
} = self; } = self;
Authentication { Authentication {
@ -258,6 +264,7 @@ impl AuthenticationUpdateInternal {
acs_reference_number: acs_reference_number.or(source.acs_reference_number), acs_reference_number: acs_reference_number.or(source.acs_reference_number),
acs_trans_id: acs_trans_id.or(source.acs_trans_id), acs_trans_id: acs_trans_id.or(source.acs_trans_id),
acs_signed_content: acs_signed_content.or(source.acs_signed_content), acs_signed_content: acs_signed_content.or(source.acs_signed_content),
ds_trans_id: ds_trans_id.or(source.ds_trans_id),
directory_server_id: directory_server_id.or(source.directory_server_id), directory_server_id: directory_server_id.or(source.directory_server_id),
..source ..source
} }
@ -336,6 +343,7 @@ impl From<AuthenticationUpdate> for AuthenticationUpdateInternal {
acs_trans_id, acs_trans_id,
acs_signed_content, acs_signed_content,
authentication_status, authentication_status,
ds_trans_id,
} => Self { } => Self {
cavv: authentication_value, cavv: authentication_value,
trans_status: Some(trans_status), trans_status: Some(trans_status),
@ -346,6 +354,7 @@ impl From<AuthenticationUpdate> for AuthenticationUpdateInternal {
acs_trans_id, acs_trans_id,
acs_signed_content, acs_signed_content,
authentication_status: Some(authentication_status), authentication_status: Some(authentication_status),
ds_trans_id,
..Default::default() ..Default::default()
}, },
AuthenticationUpdate::PostAuthenticationUpdate { AuthenticationUpdate::PostAuthenticationUpdate {

View File

@ -115,6 +115,8 @@ diesel::table! {
payment_id -> Nullable<Varchar>, payment_id -> Nullable<Varchar>,
#[max_length = 128] #[max_length = 128]
merchant_connector_id -> Varchar, merchant_connector_id -> Varchar,
#[max_length = 64]
ds_trans_id -> Nullable<Varchar>,
#[max_length = 128] #[max_length = 128]
directory_server_id -> Nullable<Varchar>, directory_server_id -> Nullable<Varchar>,
} }

View File

@ -1,7 +1,7 @@
pub mod authentication; pub mod authentication;
pub mod fraud_check; pub mod fraud_check;
use api_models::payments::RequestSurchargeDetails; use api_models::payments::RequestSurchargeDetails;
use common_utils::{consts, errors, ext_traits::OptionExt, pii}; use common_utils::{consts, errors, ext_traits::OptionExt, pii, types as common_types};
use diesel_models::enums as storage_enums; use diesel_models::enums as storage_enums;
use error_stack::ResultExt; use error_stack::ResultExt;
use masking::Secret; use masking::Secret;
@ -447,7 +447,8 @@ pub struct AuthenticationData {
pub eci: Option<String>, pub eci: Option<String>,
pub cavv: String, pub cavv: String,
pub threeds_server_transaction_id: String, pub threeds_server_transaction_id: String,
pub message_version: String, pub message_version: common_types::SemanticVersion,
pub ds_trans_id: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -219,6 +219,7 @@ pub enum AuthenticationResponseData {
authn_flow_type: AuthNFlowType, authn_flow_type: AuthNFlowType,
authentication_value: Option<String>, authentication_value: Option<String>,
trans_status: common_enums::TransactionStatus, trans_status: common_enums::TransactionStatus,
ds_trans_id: Option<String>,
}, },
PostAuthNResponse { PostAuthNResponse {
trans_status: common_enums::TransactionStatus, trans_status: common_enums::TransactionStatus,

View File

@ -383,7 +383,7 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme
eci: authentication_data.and_then(|auth| auth.eci.clone()), eci: authentication_data.and_then(|auth| auth.eci.clone()),
cryptogram: authentication_data.map(|auth| auth.cavv.clone()), cryptogram: authentication_data.map(|auth| auth.cavv.clone()),
xid: authentication_data.map(|auth| auth.threeds_server_transaction_id.clone()), xid: authentication_data.map(|auth| auth.threeds_server_transaction_id.clone()),
version: authentication_data.map(|auth| auth.message_version.clone()), version: authentication_data.map(|auth| auth.message_version.to_string()),
}, },
enums::AuthenticationType::NoThreeDs => CheckoutThreeDS { enums::AuthenticationType::NoThreeDs => CheckoutThreeDS {
enabled: false, enabled: false,

View File

@ -848,6 +848,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
if req.is_three_ds() if req.is_three_ds()
&& req.request.is_card() && req.request.is_card()
&& req.request.connector_mandate_id().is_none() && req.request.connector_mandate_id().is_none()
&& req.request.authentication_data.is_none()
{ {
Ok(format!( Ok(format!(
"{}risk/v1/authentication-setups", "{}risk/v1/authentication-setups",
@ -875,6 +876,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
if req.is_three_ds() if req.is_three_ds()
&& req.request.is_card() && req.request.is_card()
&& req.request.connector_mandate_id().is_none() && req.request.connector_mandate_id().is_none()
&& req.request.authentication_data.is_none()
{ {
let connector_req = let connector_req =
cybersource::CybersourceAuthSetupRequest::try_from(&connector_router_data)?; cybersource::CybersourceAuthSetupRequest::try_from(&connector_router_data)?;
@ -915,6 +917,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
if data.is_three_ds() if data.is_three_ds()
&& data.request.is_card() && data.request.is_card()
&& data.request.connector_mandate_id().is_none() && data.request.connector_mandate_id().is_none()
&& data.request.authentication_data.is_none()
{ {
let response: cybersource::CybersourceAuthSetupResponse = res let response: cybersource::CybersourceAuthSetupResponse = res
.response .response

View File

@ -6,7 +6,7 @@ use api_models::{
}; };
use base64::Engine; use base64::Engine;
use common_enums::FutureUsage; use common_enums::FutureUsage;
use common_utils::{ext_traits::ValueExt, pii}; use common_utils::{ext_traits::ValueExt, pii, types::SemanticVersion};
use error_stack::ResultExt; use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret}; use masking::{ExposeInterface, PeekInterface, Secret};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -261,7 +261,16 @@ pub struct CybersourceConsumerAuthInformation {
xid: Option<String>, xid: Option<String>,
directory_server_transaction_id: Option<Secret<String>>, directory_server_transaction_id: Option<Secret<String>>,
specification_version: Option<String>, specification_version: Option<String>,
/// This field specifies the 3ds version
pa_specification_version: Option<SemanticVersion>,
/// Verification response enrollment status.
///
/// This field is supported only on Asia, Middle East, and Africa Gateway.
///
/// For external authentication, this field will always be "Y"
veres_enrolled: Option<String>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MerchantDefinedInformation { pub struct MerchantDefinedInformation {
@ -655,6 +664,19 @@ impl
} else { } else {
(None, None, None) (None, None, None)
}; };
// this logic is for external authenticated card
let commerce_indicator_for_external_authentication = item
.router_data
.request
.authentication_data
.as_ref()
.and_then(|authn_data| {
authn_data
.eci
.clone()
.map(|eci| get_commerce_indicator_for_external_authentication(network, eci))
});
Ok(Self { Ok(Self {
capture: Some(matches!( capture: Some(matches!(
item.router_data.request.capture_method, item.router_data.request.capture_method,
@ -665,11 +687,62 @@ impl
action_token_types, action_token_types,
authorization_options, authorization_options,
capture_options: None, capture_options: None,
commerce_indicator, commerce_indicator: commerce_indicator_for_external_authentication
.unwrap_or(commerce_indicator),
}) })
} }
} }
fn get_commerce_indicator_for_external_authentication(
card_network: Option<String>,
eci: String,
) -> String {
let card_network_lower_case = card_network
.as_ref()
.map(|card_network| card_network.to_lowercase());
match eci.as_str() {
"00" | "01" | "02" => {
if matches!(
card_network_lower_case.as_deref(),
Some("mastercard") | Some("maestro")
) {
"spa"
} else {
"internet"
}
}
"05" => match card_network_lower_case.as_deref() {
Some("amex") => "aesk",
Some("discover") => "dipb",
Some("mastercard") => "spa",
Some("visa") => "vbv",
Some("diners") => "pb",
Some("upi") => "up3ds",
_ => "internet",
},
"06" => match card_network_lower_case.as_deref() {
Some("amex") => "aesk_attempted",
Some("discover") => "dipb_attempted",
Some("mastercard") => "spa",
Some("visa") => "vbv_attempted",
Some("diners") => "pb_attempted",
Some("upi") => "up3ds_attempted",
_ => "internet",
},
"07" => match card_network_lower_case.as_deref() {
Some("amex") => "internet",
Some("discover") => "internet",
Some("mastercard") => "spa",
Some("visa") => "vbv_failure",
Some("diners") => "internet",
Some("upi") => "up3ds_failure",
_ => "internet",
},
_ => "vbv_failure",
}
.to_string()
}
impl impl
From<( From<(
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
@ -852,12 +925,39 @@ impl
Vec::<MerchantDefinedInformation>::foreign_from(metadata.peek().to_owned()) Vec::<MerchantDefinedInformation>::foreign_from(metadata.peek().to_owned())
}); });
let consumer_authentication_information = item
.router_data
.request
.authentication_data
.as_ref()
.map(|authn_data| {
let (ucaf_authentication_data, cavv) =
if ccard.card_network == Some(common_enums::CardNetwork::Mastercard) {
(Some(Secret::new(authn_data.cavv.clone())), None)
} else {
(None, Some(authn_data.cavv.clone()))
};
CybersourceConsumerAuthInformation {
ucaf_collection_indicator: None,
cavv,
ucaf_authentication_data,
xid: Some(authn_data.threeds_server_transaction_id.clone()),
directory_server_transaction_id: authn_data
.ds_trans_id
.clone()
.map(Secret::new),
specification_version: None,
pa_specification_version: Some(authn_data.message_version.clone()),
veres_enrolled: Some("Y".to_string()),
}
});
Ok(Self { Ok(Self {
processing_information, processing_information,
payment_information, payment_information,
order_information, order_information,
client_reference_information, client_reference_information,
consumer_authentication_information: None, consumer_authentication_information,
merchant_defined_information, merchant_defined_information,
}) })
} }
@ -922,6 +1022,8 @@ impl
.three_ds_data .three_ds_data
.directory_server_transaction_id, .directory_server_transaction_id,
specification_version: three_ds_info.three_ds_data.specification_version, specification_version: three_ds_info.three_ds_data.specification_version,
pa_specification_version: None,
veres_enrolled: None,
}); });
let merchant_defined_information = let merchant_defined_information =
@ -1000,6 +1102,8 @@ impl
xid: None, xid: None,
directory_server_transaction_id: None, directory_server_transaction_id: None,
specification_version: None, specification_version: None,
pa_specification_version: None,
veres_enrolled: None,
}), }),
merchant_defined_information, merchant_defined_information,
}) })
@ -1131,6 +1235,8 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
xid: None, xid: None,
directory_server_transaction_id: None, directory_server_transaction_id: None,
specification_version: None, specification_version: None,
pa_specification_version: None,
veres_enrolled: None,
}, },
), ),
}) })

View File

@ -167,6 +167,7 @@ impl
authn_flow_type, authn_flow_type,
authentication_value: response.authentication_value, authentication_value: response.authentication_value,
trans_status: response.trans_status, trans_status: response.trans_status,
ds_trans_id: response.authentication_response.ds_trans_id,
}, },
) )
} }
@ -646,6 +647,8 @@ pub struct AuthenticationResponse {
pub acs_reference_number: Option<String>, pub acs_reference_number: Option<String>,
#[serde(rename = "acsTransID")] #[serde(rename = "acsTransID")]
pub acs_trans_id: Option<String>, pub acs_trans_id: Option<String>,
#[serde(rename = "dsTransID")]
pub ds_trans_id: Option<String>,
pub acs_signed_content: Option<String>, pub acs_signed_content: Option<String>,
} }

View File

@ -625,7 +625,7 @@ impl TryFrom<(&domain::payments::Card, &types::PaymentsAuthorizeData)> for Payme
cavv: Some(auth_data.cavv.clone()), cavv: Some(auth_data.cavv.clone()),
eci: auth_data.eci.clone(), eci: auth_data.eci.clone(),
cardholder_auth: None, cardholder_auth: None,
three_ds_version: Some(auth_data.message_version.clone()), three_ds_version: Some(auth_data.message_version.to_string()),
directory_server_id: Some(auth_data.threeds_server_transaction_id.clone().into()), directory_server_id: Some(auth_data.threeds_server_transaction_id.clone().into()),
}; };

View File

@ -187,6 +187,7 @@ impl
types::authentication::AuthNFlowType::Frictionless types::authentication::AuthNFlowType::Frictionless
}, },
authentication_value: response.authentication_value, authentication_value: response.authentication_value,
ds_trans_id: Some(response.ds_trans_id),
}, },
) )
} }

View File

@ -84,6 +84,7 @@ pub async fn update_trackers<F: Clone, Req>(
authn_flow_type, authn_flow_type,
authentication_value, authentication_value,
trans_status, trans_status,
ds_trans_id,
} => { } => {
let authentication_status = let authentication_status =
common_enums::AuthenticationStatus::foreign_from(trans_status.clone()); common_enums::AuthenticationStatus::foreign_from(trans_status.clone());
@ -97,6 +98,7 @@ pub async fn update_trackers<F: Clone, Req>(
acs_signed_content: authn_flow_type.get_acs_signed_content(), acs_signed_content: authn_flow_type.get_acs_signed_content(),
authentication_type: authn_flow_type.get_decoupled_authentication_type(), authentication_type: authn_flow_type.get_decoupled_authentication_type(),
authentication_status, authentication_status,
ds_trans_id,
} }
} }
AuthenticationResponseData::PostAuthNResponse { AuthenticationResponseData::PostAuthNResponse {
@ -183,6 +185,7 @@ pub async fn create_new_authentication(
profile_id, profile_id,
payment_id, payment_id,
merchant_connector_id, merchant_connector_id,
ds_trans_id: None,
directory_server_id: None, directory_server_id: None,
}; };
state state

View File

@ -369,7 +369,8 @@ impl ForeignTryFrom<&storage::Authentication> for AuthenticationData {
eci: authentication.eci.clone(), eci: authentication.eci.clone(),
cavv, cavv,
threeds_server_transaction_id, threeds_server_transaction_id,
message_version: message_version.to_string(), message_version,
ds_trans_id: authentication.ds_trans_id.clone(),
}) })
} else { } else {
Err(errors::ApiErrorResponse::PaymentAuthenticationFailed { data: None }.into()) Err(errors::ApiErrorResponse::PaymentAuthenticationFailed { data: None }.into())

View File

@ -147,6 +147,7 @@ impl AuthenticationInterface for MockDb {
profile_id: authentication.profile_id, profile_id: authentication.profile_id,
payment_id: authentication.payment_id, payment_id: authentication.payment_id,
merchant_connector_id: authentication.merchant_connector_id, merchant_connector_id: authentication.merchant_connector_id,
ds_trans_id: authentication.ds_trans_id,
directory_server_id: authentication.directory_server_id, directory_server_id: authentication.directory_server_id,
}; };
authentications.push(authentication.clone()); authentications.push(authentication.clone());

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE authentication DROP COLUMN If EXISTS ds_trans_id;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE authentication ADD COLUMN IF NOT EXISTS ds_trans_id VARCHAR(64);