mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
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:
@ -244,10 +244,9 @@ impl Connector {
|
||||
| Self::Riskified
|
||||
| Self::Threedsecureio
|
||||
| Self::Netcetera
|
||||
| Self::Cybersource
|
||||
| Self::Noon
|
||||
| Self::Stripe => false,
|
||||
Self::Checkout | Self::Nmi => true,
|
||||
Self::Checkout | Self::Nmi| Self::Cybersource => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ pub struct Authentication {
|
||||
pub profile_id: String,
|
||||
pub payment_id: Option<String>,
|
||||
pub merchant_connector_id: String,
|
||||
pub ds_trans_id: Option<String>,
|
||||
pub directory_server_id: Option<String>,
|
||||
}
|
||||
|
||||
@ -87,6 +88,7 @@ pub struct AuthenticationNew {
|
||||
pub profile_id: String,
|
||||
pub payment_id: Option<String>,
|
||||
pub merchant_connector_id: String,
|
||||
pub ds_trans_id: Option<String>,
|
||||
pub directory_server_id: Option<String>,
|
||||
}
|
||||
|
||||
@ -115,6 +117,7 @@ pub enum AuthenticationUpdate {
|
||||
acs_trans_id: Option<String>,
|
||||
acs_signed_content: Option<String>,
|
||||
authentication_status: common_enums::AuthenticationStatus,
|
||||
ds_trans_id: Option<String>,
|
||||
},
|
||||
PostAuthenticationUpdate {
|
||||
trans_status: common_enums::TransactionStatus,
|
||||
@ -162,6 +165,7 @@ pub struct AuthenticationUpdateInternal {
|
||||
pub acs_reference_number: Option<String>,
|
||||
pub acs_trans_id: Option<String>,
|
||||
pub acs_signed_content: Option<String>,
|
||||
pub ds_trans_id: Option<String>,
|
||||
pub directory_server_id: Option<String>,
|
||||
}
|
||||
|
||||
@ -193,6 +197,7 @@ impl Default for AuthenticationUpdateInternal {
|
||||
acs_reference_number: Default::default(),
|
||||
acs_trans_id: Default::default(),
|
||||
acs_signed_content: Default::default(),
|
||||
ds_trans_id: Default::default(),
|
||||
directory_server_id: Default::default(),
|
||||
}
|
||||
}
|
||||
@ -226,6 +231,7 @@ impl AuthenticationUpdateInternal {
|
||||
acs_reference_number,
|
||||
acs_trans_id,
|
||||
acs_signed_content,
|
||||
ds_trans_id,
|
||||
directory_server_id,
|
||||
} = self;
|
||||
Authentication {
|
||||
@ -258,6 +264,7 @@ impl AuthenticationUpdateInternal {
|
||||
acs_reference_number: acs_reference_number.or(source.acs_reference_number),
|
||||
acs_trans_id: acs_trans_id.or(source.acs_trans_id),
|
||||
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),
|
||||
..source
|
||||
}
|
||||
@ -336,6 +343,7 @@ impl From<AuthenticationUpdate> for AuthenticationUpdateInternal {
|
||||
acs_trans_id,
|
||||
acs_signed_content,
|
||||
authentication_status,
|
||||
ds_trans_id,
|
||||
} => Self {
|
||||
cavv: authentication_value,
|
||||
trans_status: Some(trans_status),
|
||||
@ -346,6 +354,7 @@ impl From<AuthenticationUpdate> for AuthenticationUpdateInternal {
|
||||
acs_trans_id,
|
||||
acs_signed_content,
|
||||
authentication_status: Some(authentication_status),
|
||||
ds_trans_id,
|
||||
..Default::default()
|
||||
},
|
||||
AuthenticationUpdate::PostAuthenticationUpdate {
|
||||
|
||||
@ -115,6 +115,8 @@ diesel::table! {
|
||||
payment_id -> Nullable<Varchar>,
|
||||
#[max_length = 128]
|
||||
merchant_connector_id -> Varchar,
|
||||
#[max_length = 64]
|
||||
ds_trans_id -> Nullable<Varchar>,
|
||||
#[max_length = 128]
|
||||
directory_server_id -> Nullable<Varchar>,
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
pub mod authentication;
|
||||
pub mod fraud_check;
|
||||
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 error_stack::ResultExt;
|
||||
use masking::Secret;
|
||||
@ -447,7 +447,8 @@ pub struct AuthenticationData {
|
||||
pub eci: Option<String>,
|
||||
pub cavv: 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)]
|
||||
|
||||
@ -219,6 +219,7 @@ pub enum AuthenticationResponseData {
|
||||
authn_flow_type: AuthNFlowType,
|
||||
authentication_value: Option<String>,
|
||||
trans_status: common_enums::TransactionStatus,
|
||||
ds_trans_id: Option<String>,
|
||||
},
|
||||
PostAuthNResponse {
|
||||
trans_status: common_enums::TransactionStatus,
|
||||
|
||||
@ -383,7 +383,7 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme
|
||||
eci: authentication_data.and_then(|auth| auth.eci.clone()),
|
||||
cryptogram: authentication_data.map(|auth| auth.cavv.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 {
|
||||
enabled: false,
|
||||
|
||||
@ -848,6 +848,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
if req.is_three_ds()
|
||||
&& req.request.is_card()
|
||||
&& req.request.connector_mandate_id().is_none()
|
||||
&& req.request.authentication_data.is_none()
|
||||
{
|
||||
Ok(format!(
|
||||
"{}risk/v1/authentication-setups",
|
||||
@ -875,6 +876,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
if req.is_three_ds()
|
||||
&& req.request.is_card()
|
||||
&& req.request.connector_mandate_id().is_none()
|
||||
&& req.request.authentication_data.is_none()
|
||||
{
|
||||
let connector_req =
|
||||
cybersource::CybersourceAuthSetupRequest::try_from(&connector_router_data)?;
|
||||
@ -915,6 +917,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
if data.is_three_ds()
|
||||
&& data.request.is_card()
|
||||
&& data.request.connector_mandate_id().is_none()
|
||||
&& data.request.authentication_data.is_none()
|
||||
{
|
||||
let response: cybersource::CybersourceAuthSetupResponse = res
|
||||
.response
|
||||
|
||||
@ -6,7 +6,7 @@ use api_models::{
|
||||
};
|
||||
use base64::Engine;
|
||||
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 masking::{ExposeInterface, PeekInterface, Secret};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -261,7 +261,16 @@ pub struct CybersourceConsumerAuthInformation {
|
||||
xid: Option<String>,
|
||||
directory_server_transaction_id: Option<Secret<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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MerchantDefinedInformation {
|
||||
@ -655,6 +664,19 @@ impl
|
||||
} else {
|
||||
(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 {
|
||||
capture: Some(matches!(
|
||||
item.router_data.request.capture_method,
|
||||
@ -665,11 +687,62 @@ impl
|
||||
action_token_types,
|
||||
authorization_options,
|
||||
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
|
||||
From<(
|
||||
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
|
||||
@ -852,12 +925,39 @@ impl
|
||||
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 {
|
||||
processing_information,
|
||||
payment_information,
|
||||
order_information,
|
||||
client_reference_information,
|
||||
consumer_authentication_information: None,
|
||||
consumer_authentication_information,
|
||||
merchant_defined_information,
|
||||
})
|
||||
}
|
||||
@ -922,6 +1022,8 @@ impl
|
||||
.three_ds_data
|
||||
.directory_server_transaction_id,
|
||||
specification_version: three_ds_info.three_ds_data.specification_version,
|
||||
pa_specification_version: None,
|
||||
veres_enrolled: None,
|
||||
});
|
||||
|
||||
let merchant_defined_information =
|
||||
@ -1000,6 +1102,8 @@ impl
|
||||
xid: None,
|
||||
directory_server_transaction_id: None,
|
||||
specification_version: None,
|
||||
pa_specification_version: None,
|
||||
veres_enrolled: None,
|
||||
}),
|
||||
merchant_defined_information,
|
||||
})
|
||||
@ -1131,6 +1235,8 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
|
||||
xid: None,
|
||||
directory_server_transaction_id: None,
|
||||
specification_version: None,
|
||||
pa_specification_version: None,
|
||||
veres_enrolled: None,
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
@ -167,6 +167,7 @@ impl
|
||||
authn_flow_type,
|
||||
authentication_value: response.authentication_value,
|
||||
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>,
|
||||
#[serde(rename = "acsTransID")]
|
||||
pub acs_trans_id: Option<String>,
|
||||
#[serde(rename = "dsTransID")]
|
||||
pub ds_trans_id: Option<String>,
|
||||
pub acs_signed_content: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@ -625,7 +625,7 @@ impl TryFrom<(&domain::payments::Card, &types::PaymentsAuthorizeData)> for Payme
|
||||
cavv: Some(auth_data.cavv.clone()),
|
||||
eci: auth_data.eci.clone(),
|
||||
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()),
|
||||
};
|
||||
|
||||
|
||||
@ -187,6 +187,7 @@ impl
|
||||
types::authentication::AuthNFlowType::Frictionless
|
||||
},
|
||||
authentication_value: response.authentication_value,
|
||||
ds_trans_id: Some(response.ds_trans_id),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -84,6 +84,7 @@ pub async fn update_trackers<F: Clone, Req>(
|
||||
authn_flow_type,
|
||||
authentication_value,
|
||||
trans_status,
|
||||
ds_trans_id,
|
||||
} => {
|
||||
let authentication_status =
|
||||
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(),
|
||||
authentication_type: authn_flow_type.get_decoupled_authentication_type(),
|
||||
authentication_status,
|
||||
ds_trans_id,
|
||||
}
|
||||
}
|
||||
AuthenticationResponseData::PostAuthNResponse {
|
||||
@ -183,6 +185,7 @@ pub async fn create_new_authentication(
|
||||
profile_id,
|
||||
payment_id,
|
||||
merchant_connector_id,
|
||||
ds_trans_id: None,
|
||||
directory_server_id: None,
|
||||
};
|
||||
state
|
||||
|
||||
@ -369,7 +369,8 @@ impl ForeignTryFrom<&storage::Authentication> for AuthenticationData {
|
||||
eci: authentication.eci.clone(),
|
||||
cavv,
|
||||
threeds_server_transaction_id,
|
||||
message_version: message_version.to_string(),
|
||||
message_version,
|
||||
ds_trans_id: authentication.ds_trans_id.clone(),
|
||||
})
|
||||
} else {
|
||||
Err(errors::ApiErrorResponse::PaymentAuthenticationFailed { data: None }.into())
|
||||
|
||||
@ -147,6 +147,7 @@ impl AuthenticationInterface for MockDb {
|
||||
profile_id: authentication.profile_id,
|
||||
payment_id: authentication.payment_id,
|
||||
merchant_connector_id: authentication.merchant_connector_id,
|
||||
ds_trans_id: authentication.ds_trans_id,
|
||||
directory_server_id: authentication.directory_server_id,
|
||||
};
|
||||
authentications.push(authentication.clone());
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
ALTER TABLE authentication DROP COLUMN If EXISTS ds_trans_id;
|
||||
@ -0,0 +1,2 @@
|
||||
-- Your SQL goes here
|
||||
ALTER TABLE authentication ADD COLUMN IF NOT EXISTS ds_trans_id VARCHAR(64);
|
||||
Reference in New Issue
Block a user