Files
Sakil Mostak 31204941ee feat(connector): [Netcetera] add sca exemption (#6611)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
2024-11-26 13:52:16 +00:00

684 lines
28 KiB
Rust

use error_stack::ResultExt;
use masking::Secret;
use serde::{Deserialize, Serialize};
use super::netcetera_types;
use crate::{
connector::utils::{self, CardData},
core::errors,
types::{self, api},
utils::OptionExt,
};
//TODO: Fill the struct with respective fields
pub struct NetceteraRouterData<T> {
pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc.
pub router_data: T,
}
impl<T> TryFrom<(&api::CurrencyUnit, types::storage::enums::Currency, i64, T)>
for NetceteraRouterData<T>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
(_currency_unit, _currency, amount, item): (
&api::CurrencyUnit,
types::storage::enums::Currency,
i64,
T,
),
) -> Result<Self, Self::Error> {
//Todo : use utils to convert the amount to the type of amount that a connector accepts
Ok(Self {
amount,
router_data: item,
})
}
}
impl<T> TryFrom<(i64, T)> for NetceteraRouterData<T> {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from((amount, router_data): (i64, T)) -> Result<Self, Self::Error> {
Ok(Self {
amount,
router_data,
})
}
}
impl
TryFrom<
types::ResponseRouterData<
api::PreAuthentication,
NetceteraPreAuthenticationResponse,
types::authentication::PreAuthNRequestData,
types::authentication::AuthenticationResponseData,
>,
> for types::authentication::PreAuthNRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
api::PreAuthentication,
NetceteraPreAuthenticationResponse,
types::authentication::PreAuthNRequestData,
types::authentication::AuthenticationResponseData,
>,
) -> Result<Self, Self::Error> {
let response = match item.response {
NetceteraPreAuthenticationResponse::Success(pre_authn_response) => {
// if card is not enrolled for 3ds, card_range will be None
let card_range = pre_authn_response.get_card_range_if_available();
let maximum_supported_3ds_version = card_range
.as_ref()
.map(|card_range| card_range.highest_common_supported_version.clone())
.unwrap_or_else(|| {
// Version "0.0.0" will be less that "2.0.0", hence we will treat this card as not eligible for 3ds authentication
common_utils::types::SemanticVersion::new(0, 0, 0)
});
let three_ds_method_data = card_range.as_ref().and_then(|card_range| {
card_range
.three_ds_method_data_form
.as_ref()
.map(|data| data.three_ds_method_data.clone())
});
let three_ds_method_url = card_range
.as_ref()
.and_then(|card_range| card_range.get_three_ds_method_url());
Ok(
types::authentication::AuthenticationResponseData::PreAuthNResponse {
threeds_server_transaction_id: pre_authn_response
.three_ds_server_trans_id
.clone(),
maximum_supported_3ds_version: maximum_supported_3ds_version.clone(),
connector_authentication_id: pre_authn_response.three_ds_server_trans_id,
three_ds_method_data,
three_ds_method_url,
message_version: maximum_supported_3ds_version,
connector_metadata: None,
directory_server_id: card_range
.as_ref()
.and_then(|card_range| card_range.directory_server_id.clone()),
},
)
}
NetceteraPreAuthenticationResponse::Failure(error_response) => {
Err(types::ErrorResponse {
code: error_response.error_details.error_code,
message: error_response.error_details.error_description,
reason: error_response.error_details.error_detail,
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: None,
})
}
};
Ok(Self {
response,
..item.data.clone()
})
}
}
impl
TryFrom<
types::ResponseRouterData<
api::Authentication,
NetceteraAuthenticationResponse,
types::authentication::ConnectorAuthenticationRequestData,
types::authentication::AuthenticationResponseData,
>,
> for types::authentication::ConnectorAuthenticationRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
api::Authentication,
NetceteraAuthenticationResponse,
types::authentication::ConnectorAuthenticationRequestData,
types::authentication::AuthenticationResponseData,
>,
) -> Result<Self, Self::Error> {
let response = match item.response {
NetceteraAuthenticationResponse::Success(response) => {
let authn_flow_type = match response.acs_challenge_mandated {
Some(ACSChallengeMandatedIndicator::Y) => {
types::authentication::AuthNFlowType::Challenge(Box::new(
types::authentication::ChallengeParams {
acs_url: response.authentication_response.acs_url.clone(),
challenge_request: response.encoded_challenge_request,
acs_reference_number: response
.authentication_response
.acs_reference_number,
acs_trans_id: response.authentication_response.acs_trans_id,
three_dsserver_trans_id: Some(response.three_ds_server_trans_id),
acs_signed_content: response
.authentication_response
.acs_signed_content,
},
))
}
Some(ACSChallengeMandatedIndicator::N) | None => {
types::authentication::AuthNFlowType::Frictionless
}
};
Ok(
types::authentication::AuthenticationResponseData::AuthNResponse {
authn_flow_type,
authentication_value: response.authentication_value,
trans_status: response.trans_status,
connector_metadata: None,
ds_trans_id: response.authentication_response.ds_trans_id,
},
)
}
NetceteraAuthenticationResponse::Error(error_response) => Err(types::ErrorResponse {
code: error_response.error_details.error_code,
message: error_response.error_details.error_description,
reason: error_response.error_details.error_detail,
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: None,
}),
};
Ok(Self {
response,
..item.data.clone()
})
}
}
pub struct NetceteraAuthType {
pub(super) certificate: Secret<String>,
pub(super) private_key: Secret<String>,
}
impl TryFrom<&types::ConnectorAuthType> for NetceteraAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type.to_owned() {
types::ConnectorAuthType::CertificateAuth {
certificate,
private_key,
} => Ok(Self {
certificate,
private_key,
}),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetceteraErrorResponse {
pub three_ds_server_trans_id: Option<String>,
pub error_details: NetceteraErrorDetails,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetceteraErrorDetails {
/// Universally unique identifier for the transaction assigned by the 3DS Server.
#[serde(rename = "threeDSServerTransID")]
pub three_ds_server_trans_id: Option<String>,
/// Universally Unique identifier for the transaction assigned by the ACS.
#[serde(rename = "acsTransID")]
pub acs_trans_id: Option<String>,
/// Universally unique identifier for the transaction assigned by the DS.
#[serde(rename = "dsTransID")]
pub ds_trans_id: Option<String>,
/// Code indicating the type of problem identified.
pub error_code: String,
/// Code indicating the 3-D Secure component that identified the error.
pub error_component: Option<String>,
/// Text describing the problem identified.
pub error_description: String,
/// Additional detail regarding the problem identified.
pub error_detail: Option<String>,
/// Universally unique identifier for the transaction assigned by the 3DS SDK.
#[serde(rename = "sdkTransID")]
pub sdk_trans_id: Option<String>,
/// The Message Type that was identified as erroneous.
pub error_message_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NetceteraMetaData {
pub mcc: String,
pub merchant_country_code: String,
pub merchant_name: String,
pub endpoint_prefix: String,
pub three_ds_requestor_name: String,
pub three_ds_requestor_id: String,
}
impl TryFrom<&Option<common_utils::pii::SecretSerdeValue>> for NetceteraMetaData {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
meta_data: &Option<common_utils::pii::SecretSerdeValue>,
) -> Result<Self, Self::Error> {
let metadata: Self = utils::to_connector_meta_from_secret::<Self>(meta_data.clone())
.change_context(errors::ConnectorError::InvalidConnectorConfig {
config: "metadata",
})?;
Ok(metadata)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetceteraPreAuthenticationRequest {
cardholder_account_number: cards::CardNumber,
scheme_id: Option<SchemeId>,
}
#[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,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum NetceteraPreAuthenticationResponse {
Success(Box<NetceteraPreAuthenticationResponseData>),
Failure(Box<NetceteraErrorResponse>),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetceteraPreAuthenticationResponseData {
#[serde(rename = "threeDSServerTransID")]
pub three_ds_server_trans_id: String,
pub card_ranges: Vec<CardRange>,
}
impl NetceteraPreAuthenticationResponseData {
pub fn get_card_range_if_available(&self) -> Option<CardRange> {
let card_range = self
.card_ranges
.iter()
.max_by_key(|card_range| &card_range.highest_common_supported_version);
card_range.cloned()
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CardRange {
pub scheme_id: SchemeId,
pub directory_server_id: Option<String>,
pub acs_protocol_versions: Vec<AcsProtocolVersion>,
#[serde(rename = "threeDSMethodDataForm")]
pub three_ds_method_data_form: Option<ThreeDSMethodDataForm>,
pub highest_common_supported_version: common_utils::types::SemanticVersion,
}
impl CardRange {
pub fn get_three_ds_method_url(&self) -> Option<String> {
self.acs_protocol_versions
.iter()
.find(|acs_protocol_version| {
acs_protocol_version.version == self.highest_common_supported_version
})
.and_then(|acs_version| acs_version.three_ds_method_url.clone())
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ThreeDSMethodDataForm {
// base64 encoded value for 3ds method data collection
#[serde(rename = "threeDSMethodData")]
pub three_ds_method_data: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AcsProtocolVersion {
pub version: common_utils::types::SemanticVersion,
#[serde(rename = "threeDSMethodURL")]
pub three_ds_method_url: Option<String>,
}
impl TryFrom<&NetceteraRouterData<&types::authentication::PreAuthNRouterData>>
for NetceteraPreAuthenticationRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
value: &NetceteraRouterData<&types::authentication::PreAuthNRouterData>,
) -> Result<Self, Self::Error> {
let router_data = value.router_data;
Ok(Self {
cardholder_account_number: router_data.request.card_holder_account_number.clone(),
scheme_id: None,
})
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
#[serde_with::skip_serializing_none]
pub struct NetceteraAuthenticationRequest {
/// Specifies the preferred version of 3D Secure protocol to be utilized while executing 3D Secure authentication.
/// 3DS Server initiates an authentication request with the preferred version and if this version is not supported by
/// other 3D Secure components, it falls back to the next supported version(s) and continues authentication.
///
/// If the preferred version is enforced by setting #enforcePreferredProtocolVersion flag, but this version
/// is not supported by one of the 3D Secure components, 3DS Server does not initiate an authentication and provides
/// corresponding error message to the customer.
///
/// The accepted values are:
/// - 2.1.0 -> prefer authentication with 2.1.0 version,
/// - 2.2.0 -> prefer authentication with 2.2.0 version,
/// - 2.3.1 -> prefer authentication with 2.3.1 version,
/// - latest -> prefer authentication with the latest version, the 3DS Server is certified for. 2.3.1 at this moment.
pub preferred_protocol_version: Option<common_utils::types::SemanticVersion>,
/// Boolean flag that enforces preferred 3D Secure protocol version to be used in 3D Secure authentication.
/// The value should be set true to enforce preferred version. If value is false or not provided,
/// 3DS Server can fall back to next supported 3DS protocol version while initiating 3D Secure authentication.
///
/// For application initiated transactions (deviceChannel = '01'), the preferred protocol version must be enforced.
pub enforce_preferred_protocol_version: Option<bool>,
pub device_channel: netcetera_types::NetceteraDeviceChannel,
/// Identifies the category of the message for a specific use case. The accepted values are:
///
/// - 01 -> PA
/// - 02 -> NPA
/// - 80 - 99 -> PS Specific Values (80 -> MasterCard Identity Check Insights;
/// 85 -> MasterCard Identity Check, Production Validation PA;
/// 86 -> MasterCard Identity Check, Production Validation NPA)
pub message_category: netcetera_types::NetceteraMessageCategory,
#[serde(rename = "threeDSCompInd")]
pub three_ds_comp_ind: Option<netcetera_types::ThreeDSMethodCompletionIndicator>,
/**
* Contains the 3DS Server Transaction ID used during the previous execution of the 3DS method. Accepted value
* length is 36 characters. Accepted value is a Canonical format as defined in IETF RFC 4122. May utilise any of the
* specified versions if the output meets specified requirements.
*
* This field is required if the 3DS Requestor reuses previous 3DS Method execution with deviceChannel = 02 (BRW).
* Available for supporting EMV 3DS 2.3.1 and later versions.
*/
#[serde(rename = "threeDSMethodId")]
pub three_ds_method_id: Option<String>,
#[serde(rename = "threeDSRequestor")]
pub three_ds_requestor: Option<netcetera_types::ThreeDSRequestor>,
#[serde(rename = "threeDSServerTransID")]
pub three_ds_server_trans_id: String,
#[serde(rename = "threeDSRequestorURL")]
pub three_ds_requestor_url: Option<String>,
pub cardholder_account: netcetera_types::CardholderAccount,
pub cardholder: Option<netcetera_types::Cardholder>,
pub purchase: Option<netcetera_types::Purchase>,
pub acquirer: Option<netcetera_types::AcquirerData>,
pub merchant: Option<netcetera_types::MerchantData>,
pub broad_info: Option<String>,
pub device_render_options: Option<netcetera_types::DeviceRenderingOptionsSupported>,
pub message_extension: Option<Vec<netcetera_types::MessageExtensionAttribute>>,
pub challenge_message_extension: Option<Vec<netcetera_types::MessageExtensionAttribute>>,
pub browser_information: Option<netcetera_types::Browser>,
#[serde(rename = "threeRIInd")]
pub three_ri_ind: Option<String>,
pub sdk_information: Option<netcetera_types::Sdk>,
pub device: Option<String>,
pub multi_transaction: Option<String>,
pub device_id: Option<String>,
pub user_id: Option<String>,
pub payee_origin: Option<url::Url>,
}
impl TryFrom<&NetceteraRouterData<&types::authentication::ConnectorAuthenticationRouterData>>
for NetceteraAuthenticationRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &NetceteraRouterData<&types::authentication::ConnectorAuthenticationRouterData>,
) -> Result<Self, Self::Error> {
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 card = utils::get_card_details(request.payment_method_data, "netcetera")?;
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,
acct_id: None,
pay_token_ind: None,
pay_token_info: None,
card_security_code: Some(card.card_cvc),
};
let currency = request
.currency
.get_required_value("currency")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "currency",
})?;
let purchase = netcetera_types::Purchase {
purchase_instal_data: None,
merchant_risk_indicator: None,
purchase_amount: request.amount,
purchase_currency: currency.iso_4217().to_string(),
purchase_exponent: currency.number_of_digits_after_decimal_point(),
purchase_date: Some(
common_utils::date_time::format_date(
now,
common_utils::date_time::DateFormat::YYYYMMDDHHmmss,
)
.change_context(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Failed to format Date".to_string(),
),
)?,
),
recurring_expiry: None,
recurring_frequency: None,
trans_type: None,
recurring_amount: None,
recurring_currency: None,
recurring_exponent: None,
recurring_date: None,
amount_ind: None,
frequency_ind: None,
};
let acquirer_details = netcetera_types::AcquirerData {
acquirer_bin: request.pre_authentication_data.acquirer_bin,
acquirer_merchant_id: request.pre_authentication_data.acquirer_merchant_id,
acquirer_country_code: request.pre_authentication_data.acquirer_country_code,
};
let connector_meta_data: NetceteraMetaData = item
.router_data
.connector_meta_data
.clone()
.parse_value("NetceteraMetaData")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_data = netcetera_types::MerchantData {
merchant_configuration_id: None,
mcc: Some(connector_meta_data.mcc),
merchant_country_code: Some(connector_meta_data.merchant_country_code),
merchant_name: Some(connector_meta_data.merchant_name),
notification_url: request.return_url.clone(),
three_ds_requestor_id: Some(connector_meta_data.three_ds_requestor_id),
three_ds_requestor_name: Some(connector_meta_data.three_ds_requestor_name),
white_list_status: None,
trust_list_status: None,
seller_info: None,
results_response_notification_url: Some(request.webhook_url),
};
let browser_information = match request.device_channel {
api_models::payments::DeviceChannel::Browser => {
request.browser_details.map(netcetera_types::Browser::from)
}
api_models::payments::DeviceChannel::App => None,
};
let sdk_information = match request.device_channel {
api_models::payments::DeviceChannel::App => {
request.sdk_information.map(netcetera_types::Sdk::from)
}
api_models::payments::DeviceChannel::Browser => None,
};
let device_render_options = match request.device_channel {
api_models::payments::DeviceChannel::App => {
Some(netcetera_types::DeviceRenderingOptionsSupported {
// hard-coded until core provides these values.
sdk_interface: netcetera_types::SdkInterface::Both,
sdk_ui_type: vec![
netcetera_types::SdkUiType::Text,
netcetera_types::SdkUiType::SingleSelect,
netcetera_types::SdkUiType::MultiSelect,
netcetera_types::SdkUiType::Oob,
netcetera_types::SdkUiType::HtmlOther,
],
})
}
api_models::payments::DeviceChannel::Browser => None,
};
Ok(Self {
preferred_protocol_version: Some(pre_authn_data.message_version),
enforce_preferred_protocol_version: None,
device_channel: netcetera_types::NetceteraDeviceChannel::from(request.device_channel),
message_category: netcetera_types::NetceteraMessageCategory::from(
request.message_category,
),
three_ds_comp_ind: Some(netcetera_types::ThreeDSMethodCompletionIndicator::from(
request.threeds_method_comp_ind,
)),
three_ds_method_id: None,
three_ds_requestor: Some(three_ds_requestor),
three_ds_server_trans_id: pre_authn_data.threeds_server_transaction_id,
three_ds_requestor_url: Some(request.three_ds_requestor_url),
cardholder_account,
cardholder: Some(netcetera_types::Cardholder::try_from((
request.billing_address,
request.shipping_address,
))?),
purchase: Some(purchase),
acquirer: Some(acquirer_details),
merchant: Some(merchant_data),
broad_info: None,
device_render_options,
message_extension: None,
challenge_message_extension: None,
browser_information,
three_ri_ind: None,
sdk_information,
device: None,
multi_transaction: None,
device_id: None,
user_id: None,
payee_origin: None,
})
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum NetceteraAuthenticationResponse {
Error(NetceteraAuthenticationFailureResponse),
Success(NetceteraAuthenticationSuccessResponse),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetceteraAuthenticationSuccessResponse {
#[serde(rename = "threeDSServerTransID")]
pub three_ds_server_trans_id: String,
pub trans_status: common_enums::TransactionStatus,
pub authentication_value: Option<String>,
pub eci: Option<String>,
pub acs_challenge_mandated: Option<ACSChallengeMandatedIndicator>,
pub authentication_response: AuthenticationResponse,
#[serde(rename = "base64EncodedChallengeRequest")]
pub encoded_challenge_request: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetceteraAuthenticationFailureResponse {
pub error_details: NetceteraErrorDetails,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticationResponse {
#[serde(rename = "acsURL")]
pub acs_url: Option<url::Url>,
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>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub enum ACSChallengeMandatedIndicator {
/// Challenge is mandated
Y,
/// Challenge is not mandated
N,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResultsResponseData {
/// Universally unique transaction identifier assigned by the 3DS Server to identify a single transaction.
/// It has the same value as the authentication request and conforms to the format defined in IETF RFC 4122.
#[serde(rename = "threeDSServerTransID")]
pub three_ds_server_trans_id: String,
/// Indicates the status of a transaction in terms of its authentication.
///
/// Valid values:
/// - `Y`: Authentication / Account verification successful.
/// - `N`: Not authenticated / Account not verified; Transaction denied.
/// - `U`: Authentication / Account verification could not be performed; technical or other problem.
/// - `C`: A challenge is required to complete the authentication.
/// - `R`: Authentication / Account verification Rejected. Issuer is rejecting authentication/verification
/// and request that authorization not be attempted.
/// - `A`: Attempts processing performed; Not authenticated / verified, but a proof of attempt
/// authentication / verification is provided.
/// - `D`: A challenge is required to complete the authentication. Decoupled Authentication confirmed.
/// - `I`: Informational Only; 3DS Requestor challenge preference acknowledged.
pub trans_status: Option<common_enums::TransactionStatus>,
/// Payment System-specific value provided as part of the ACS registration for each supported DS.
/// Authentication Value may be used to provide proof of authentication.
pub authentication_value: Option<String>,
/// Payment System-specific value provided by the ACS to indicate the results of the attempt to authenticate
/// the Cardholder.
pub eci: Option<String>,
/// The received Results Request from the Directory Server.
pub results_request: Option<serde_json::Value>,
/// The sent Results Response to the Directory Server.
pub results_response: Option<serde_json::Value>,
/// Optional object containing error details if any errors occurred during the process.
pub error_details: Option<NetceteraErrorDetails>,
}