feat(connector): [Nuvei] add support for bank redirect Eps, Sofort, Giropay, Ideal (#870)

Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
Jagan
2023-04-21 12:51:06 +05:30
committed by GitHub
parent ccc0c3f960
commit c1a25b30bd
12 changed files with 1162 additions and 152 deletions

157
Cargo.lock generated
View File

@ -226,7 +226,7 @@ dependencies = [
"serde_urlencoded",
"smallvec",
"socket2",
"time",
"time 0.3.20",
"url",
]
@ -338,7 +338,7 @@ dependencies = [
"serde",
"serde_json",
"strum",
"time",
"time 0.3.20",
"url",
"utoipa",
]
@ -550,7 +550,7 @@ dependencies = [
"http",
"hyper",
"ring",
"time",
"time 0.3.20",
"tokio",
"tower",
"tracing",
@ -709,7 +709,7 @@ dependencies = [
"percent-encoding",
"regex",
"sha2",
"time",
"time 0.3.20",
"tracing",
]
@ -816,7 +816,7 @@ dependencies = [
"itoa",
"num-integer",
"ryu",
"time",
"time 0.3.20",
]
[[package]]
@ -1099,9 +1099,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"serde",
"time 0.1.43",
"wasm-bindgen",
"winapi",
]
@ -1191,7 +1194,7 @@ dependencies = [
"signal-hook",
"signal-hook-tokio",
"thiserror",
"time",
"time 0.3.20",
"tokio",
]
@ -1242,7 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
"time",
"time 0.3.20",
"version_check",
]
@ -1482,7 +1485,7 @@ dependencies = [
"pq-sys",
"r2d2",
"serde_json",
"time",
"time 0.3.20",
]
[[package]]
@ -1662,6 +1665,28 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "fantoccini"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64"
dependencies = [
"base64 0.13.1",
"cookie",
"futures-core",
"futures-util",
"http",
"hyper",
"hyper-rustls",
"mime",
"serde",
"serde_json",
"time 0.3.20",
"tokio",
"url",
"webdriver",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@ -2308,7 +2333,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror",
"time",
"time 0.3.20",
]
[[package]]
@ -3486,8 +3511,9 @@ dependencies = [
"signal-hook-tokio",
"storage_models",
"strum",
"thirtyfour",
"thiserror",
"time",
"time 0.3.20",
"tokio",
"toml 0.7.3",
"url",
@ -3526,7 +3552,7 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"strum",
"time",
"time 0.3.20",
"tokio",
"tracing",
"tracing-actix-web",
@ -3812,6 +3838,17 @@ dependencies = [
"thiserror",
]
[[package]]
name = "serde_repr"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.11",
]
[[package]]
name = "serde_spanned"
version = "0.6.1"
@ -3846,7 +3883,7 @@ dependencies = [
"serde",
"serde_json",
"serde_with_macros",
"time",
"time 0.3.20",
]
[[package]]
@ -3977,7 +4014,7 @@ dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time",
"time 0.3.20",
]
[[package]]
@ -4046,7 +4083,16 @@ dependencies = [
"serde_json",
"strum",
"thiserror",
"time",
"time 0.3.20",
]
[[package]]
name = "stringmatch"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aadc0801d92f0cdc26127c67c4b8766284f52a5ba22894f285e3101fa57d05d"
dependencies = [
"regex",
]
[[package]]
@ -4139,6 +4185,44 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "thirtyfour"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72fc70ad9624071cdd96d034676b84b504bfeb4bee1580df1324c99373ea0ca7"
dependencies = [
"async-trait",
"base64 0.13.1",
"chrono",
"cookie",
"fantoccini",
"futures",
"http",
"log",
"parking_lot",
"serde",
"serde_json",
"serde_repr",
"stringmatch",
"thirtyfour-macros",
"thiserror",
"tokio",
"url",
"urlparse",
]
[[package]]
name = "thirtyfour-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cae91d1c7c61ec65817f1064954640ee350a50ae6548ff9a1bdd2489d6ffbb0"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "thiserror"
version = "1.0.40"
@ -4169,6 +4253,16 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "time"
version = "0.3.20"
@ -4438,7 +4532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e"
dependencies = [
"crossbeam-channel",
"time",
"time 0.3.20",
"tracing-subscriber",
]
@ -4589,6 +4683,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.10"
@ -4619,6 +4719,12 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "urlparse"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517"
[[package]]
name = "utoipa"
version = "3.3.0"
@ -4691,7 +4797,7 @@ dependencies = [
"git2",
"rustc_version",
"rustversion",
"time",
"time 0.3.20",
]
[[package]]
@ -4829,6 +4935,25 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webdriver"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f"
dependencies = [
"base64 0.13.1",
"bytes",
"cookie",
"http",
"log",
"serde",
"serde_derive",
"serde_json",
"time 0.3.20",
"unicode-segmentation",
"url",
]
[[package]]
name = "webpki"
version = "0.22.0"

View File

@ -628,6 +628,7 @@ pub enum WalletData {
}
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
#[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))]
pub struct GooglePayWalletData {
/// The type of payment method
#[serde(rename = "type")]
@ -659,6 +660,7 @@ pub struct MbWayRedirection {
}
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
#[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))]
pub struct GooglePayPaymentMethodInfo {
/// The name of the card network
pub card_network: String,

View File

@ -102,6 +102,7 @@ time = { version = "0.3.20", features = ["macros"] }
tokio = "1.27.0"
toml = "0.7.3"
wiremock = "0.5"
thirtyfour = "0.31.0"
[[bin]]
name = "router"

View File

@ -10,7 +10,7 @@ use ::common_utils::{
use error_stack::{IntoReport, ResultExt};
use transformers as nuvei;
use super::utils::{self, to_boolean, RouterData};
use super::utils::{self, RouterData};
use crate::{
configs::settings,
core::{
@ -486,8 +486,12 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
)
.await?;
router_data.session_token = resp.session_token;
let (enrolled_for_3ds, related_transaction_id) = match router_data.auth_type {
storage_models::enums::AuthenticationType::ThreeDs => {
let (enrolled_for_3ds, related_transaction_id) =
match (router_data.auth_type, router_data.payment_method) {
(
storage_models::enums::AuthenticationType::ThreeDs,
storage_models::enums::PaymentMethod::Card,
) => {
let integ: Box<
&(dyn ConnectorIntegration<
InitPayment,
@ -508,12 +512,15 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
payments::CallConnectorAction::Trigger,
)
.await?;
(
init_resp.request.enrolled_for_3ds,
init_resp.request.related_transaction_id,
)
match init_resp.response {
Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse {
enrolled_v2,
related_transaction_id,
}) => (enrolled_v2, related_transaction_id),
_ => (false, None),
}
storage_models::enums::AuthenticationType::NoThreeDs => (false, None),
}
_ => (false, None),
};
router_data.request.enrolled_for_3ds = enrolled_for_3ds;
@ -725,28 +732,12 @@ impl ConnectorIntegration<InitPayment, types::PaymentsAuthorizeData, types::Paym
.response
.parse_struct("NuveiPaymentsResponse")
.switch()?;
let response_data = types::RouterData::try_from(types::ResponseRouterData {
response: response.clone(),
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
let is_enrolled_for_3ds = response
.clone()
.payment_option
.and_then(|po| po.card)
.and_then(|c| c.three_d)
.and_then(|t| t.v2supported)
.map(to_boolean)
.unwrap_or_default();
Ok(types::RouterData {
request: types::PaymentsAuthorizeData {
enrolled_for_3ds: is_enrolled_for_3ds,
related_transaction_id: response.transaction_id,
..response_data.request
},
..response_data
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(

View File

@ -1,3 +1,4 @@
use api_models::payments;
use common_utils::{
crypto::{self, GenerateDigest},
date_time, fp_utils,
@ -10,12 +11,13 @@ use serde::{Deserialize, Serialize};
use crate::{
connector::utils::{
self, MandateData, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, RouterData,
self, AddressDetailsData, MandateData, PaymentsAuthorizeRequestData,
PaymentsCancelRequestData, RouterData,
},
consts,
core::errors,
services,
types::{self, api, storage::enums},
types::{self, api, storage::enums, transformers::ForeignTryFrom},
};
#[derive(Debug, Serialize, Default, Deserialize)]
@ -62,7 +64,7 @@ pub struct NuveiPaymentsRequest {
pub merchant_site_id: String,
pub client_request_id: String,
pub amount: String,
pub currency: String,
pub currency: storage_models::enums::Currency,
/// This ID uniquely identifies your consumer/user in your system.
pub user_token_id: Option<Secret<String, Email>>,
pub client_unique_id: String,
@ -95,7 +97,7 @@ pub struct NuveiPaymentFlowRequest {
pub merchant_site_id: String,
pub client_request_id: String,
pub amount: String,
pub currency: String,
pub currency: storage_models::enums::Currency,
pub related_transaction_id: Option<String>,
pub checksum: String,
}
@ -125,22 +127,65 @@ pub struct PaymentOption {
pub billing_address: Option<BillingAddress>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NuveiBIC {
#[serde(rename = "ABNANL2A")]
Abnamro,
#[serde(rename = "ASNBNL21")]
ASNBank,
#[serde(rename = "BUNQNL2A")]
Bunq,
#[serde(rename = "INGBNL2A")]
Ing,
#[serde(rename = "KNABNL2H")]
Knab,
#[serde(rename = "RABONL2U")]
Rabobank,
#[serde(rename = "RBRBNL21")]
RegioBank,
#[serde(rename = "SNSBNL2A")]
SNSBank,
#[serde(rename = "TRIONL2U")]
TriodosBank,
#[serde(rename = "FVLBNL22")]
VanLanschotBankiers,
#[serde(rename = "MOYONL21")]
Moneyou,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AlternativePaymentMethod {
pub payment_method: AlternativePaymentMethodType,
#[serde(rename = "BIC")]
pub bank_id: Option<NuveiBIC>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AlternativePaymentMethodType {
#[default]
ApmgwExpresscheckout,
#[serde(rename = "apmgw_expresscheckout")]
Expresscheckout,
#[serde(rename = "apmgw_Giropay")]
Giropay,
#[serde(rename = "apmgw_Sofort")]
Sofort,
#[serde(rename = "apmgw_iDeal")]
Ideal,
#[serde(rename = "apmgw_EPS")]
Eps,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BillingAddress {
pub email: Secret<String, Email>,
pub email: Option<Secret<String, Email>>,
pub first_name: Option<Secret<String>>,
pub last_name: Option<Secret<String>>,
pub country: api_models::enums::CountryCode,
}
@ -366,28 +411,34 @@ impl<F, T>
#[derive(Debug, Default)]
pub struct NuveiCardDetails {
card: api_models::payments::Card,
card: payments::Card,
three_d: Option<ThreeD>,
}
impl From<api_models::payments::GooglePayWalletData> for NuveiPaymentsRequest {
fn from(gpay_data: api_models::payments::GooglePayWalletData) -> Self {
Self {
impl TryFrom<payments::GooglePayWalletData> for NuveiPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(gpay_data: payments::GooglePayWalletData) -> Result<Self, Self::Error> {
Ok(Self {
payment_option: PaymentOption {
card: Some(Card {
external_token: Some(ExternalToken {
external_token_provider: ExternalTokenProvider::GooglePay,
mobile_token: gpay_data.tokenization_data.token,
mobile_token: common_utils::ext_traits::Encode::<
payments::GooglePayWalletData,
>::encode_to_string_of_json(
&gpay_data
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?,
}),
..Default::default()
}),
..Default::default()
},
..Default::default()
})
}
}
}
impl From<api_models::payments::ApplePayWalletData> for NuveiPaymentsRequest {
fn from(apple_pay_data: api_models::payments::ApplePayWalletData) -> Self {
impl From<payments::ApplePayWalletData> for NuveiPaymentsRequest {
fn from(apple_pay_data: payments::ApplePayWalletData) -> Self {
Self {
payment_option: PaymentOption {
card: Some(Card {
@ -404,6 +455,109 @@ impl From<api_models::payments::ApplePayWalletData> for NuveiPaymentsRequest {
}
}
impl TryFrom<api_models::enums::BankNames> for NuveiBIC {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(bank: api_models::enums::BankNames) -> Result<Self, Self::Error> {
match bank {
api_models::enums::BankNames::AbnAmro => Ok(Self::Abnamro),
api_models::enums::BankNames::AsnBank => Ok(Self::ASNBank),
api_models::enums::BankNames::Bunq => Ok(Self::Bunq),
api_models::enums::BankNames::Ing => Ok(Self::Ing),
api_models::enums::BankNames::Knab => Ok(Self::Knab),
api_models::enums::BankNames::Rabobank => Ok(Self::Rabobank),
api_models::enums::BankNames::SnsBank => Ok(Self::SNSBank),
api_models::enums::BankNames::TriodosBank => Ok(Self::TriodosBank),
api_models::enums::BankNames::VanLanschot => Ok(Self::VanLanschotBankiers),
api_models::enums::BankNames::Moneyou => Ok(Self::Moneyou),
_ => Err(errors::ConnectorError::FlowNotSupported {
flow: bank.to_string(),
connector: "Nuvei".to_string(),
}
.into()),
}
}
}
impl<F>
ForeignTryFrom<(
AlternativePaymentMethodType,
Option<payments::BankRedirectData>,
&types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
)> for NuveiPaymentsRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn foreign_try_from(
data: (
AlternativePaymentMethodType,
Option<payments::BankRedirectData>,
&types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
),
) -> Result<Self, Self::Error> {
let (payment_method, redirect, item) = data;
let (billing_address, bank_id) = match (&payment_method, redirect) {
(AlternativePaymentMethodType::Expresscheckout, _) => (
Some(BillingAddress {
email: Some(item.request.get_email()?),
country: item.get_billing_country()?,
..Default::default()
}),
None,
),
(AlternativePaymentMethodType::Giropay, _) => (
Some(BillingAddress {
email: Some(item.request.get_email()?),
country: item.get_billing_country()?,
..Default::default()
}),
None,
),
(AlternativePaymentMethodType::Sofort, _) | (AlternativePaymentMethodType::Eps, _) => {
let address = item.get_billing_address()?;
(
Some(BillingAddress {
first_name: Some(address.get_first_name()?.clone()),
last_name: Some(address.get_last_name()?.clone()),
email: Some(item.request.get_email()?),
country: item.get_billing_country()?,
}),
None,
)
}
(
AlternativePaymentMethodType::Ideal,
Some(payments::BankRedirectData::Ideal { bank_name, .. }),
) => {
let address = item.get_billing_address()?;
(
Some(BillingAddress {
first_name: Some(address.get_first_name()?.clone()),
last_name: Some(address.get_last_name()?.clone()),
email: Some(item.request.get_email()?),
country: item.get_billing_country()?,
}),
Some(NuveiBIC::try_from(bank_name)?),
)
}
_ => Err(errors::ConnectorError::NotSupported {
payment_method: "Bank Redirect".to_string(),
connector: "Nuvei",
payment_experience: "Redirection".to_string(),
})?,
};
Ok(Self {
payment_option: PaymentOption {
alternative_payment_method: Some(AlternativePaymentMethod {
payment_method,
bank_id,
}),
..Default::default()
},
billing_address,
..Default::default()
})
}
}
impl<F>
TryFrom<(
&types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
@ -421,23 +575,13 @@ impl<F>
let request_data = match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(card) => get_card_info(item, &card),
api::PaymentMethodData::Wallet(wallet) => match wallet {
api_models::payments::WalletData::GooglePay(gpay_data) => Ok(Self::from(gpay_data)),
api_models::payments::WalletData::ApplePay(apple_pay_data) => {
Ok(Self::from(apple_pay_data))
}
api_models::payments::WalletData::PaypalRedirect(_) => Ok(Self {
payment_option: PaymentOption {
alternative_payment_method: Some(AlternativePaymentMethod {
payment_method: AlternativePaymentMethodType::ApmgwExpresscheckout,
}),
..Default::default()
},
billing_address: Some(BillingAddress {
email: item.request.get_email()?,
country: item.get_billing_country()?,
}),
..Default::default()
}),
payments::WalletData::GooglePay(gpay_data) => Self::try_from(gpay_data),
payments::WalletData::ApplePay(apple_pay_data) => Ok(Self::from(apple_pay_data)),
payments::WalletData::PaypalRedirect(_) => Self::foreign_try_from((
AlternativePaymentMethodType::Expresscheckout,
None,
item,
)),
_ => Err(errors::ConnectorError::NotSupported {
payment_method: "Wallet".to_string(),
connector: "Nuvei",
@ -445,11 +589,39 @@ impl<F>
}
.into()),
},
api::PaymentMethodData::BankRedirect(redirect) => match redirect {
payments::BankRedirectData::Eps { .. } => Self::foreign_try_from((
AlternativePaymentMethodType::Eps,
Some(redirect),
item,
)),
payments::BankRedirectData::Giropay { .. } => Self::foreign_try_from((
AlternativePaymentMethodType::Giropay,
Some(redirect),
item,
)),
payments::BankRedirectData::Ideal { .. } => Self::foreign_try_from((
AlternativePaymentMethodType::Ideal,
Some(redirect),
item,
)),
payments::BankRedirectData::Sofort { .. } => Self::foreign_try_from((
AlternativePaymentMethodType::Sofort,
Some(redirect),
item,
)),
_ => Err(errors::ConnectorError::NotSupported {
payment_method: "Bank Redirect".to_string(),
connector: "Nuvei",
payment_experience: "RedirectToUrl".to_string(),
}
.into()),
},
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
}?;
let request = Self::try_from(NuveiPaymentRequestData {
amount: item.request.amount.clone().to_string(),
currency: item.request.currency.clone().to_string(),
amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
currency: item.request.currency,
connector_auth_type: item.connector_auth_type.clone(),
client_request_id: item.attempt_id.clone(),
session_token: data.1,
@ -461,6 +633,7 @@ impl<F>
user_token_id: request_data.user_token_id,
related_transaction_id: request_data.related_transaction_id,
payment_option: request_data.payment_option,
billing_address: request_data.billing_address,
..request
})
}
@ -468,7 +641,7 @@ impl<F>
fn get_card_info<F>(
item: &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
card_details: &api_models::payments::Card,
card_details: &payments::Card,
) -> Result<NuveiPaymentsRequest, error_stack::Report<errors::ConnectorError>> {
let browser_info = item.request.get_browser_info()?;
let related_transaction_id = if item.is_three_ds() {
@ -493,8 +666,8 @@ fn get_card_info<F>(
match item.request.setup_mandate_details.clone() {
Some(mandate_data) => {
let details = match mandate_data.mandate_type {
api_models::payments::MandateType::SingleUse(details) => details,
api_models::payments::MandateType::MultiUse(details) => {
payments::MandateType::SingleUse(details) => details,
payments::MandateType::MultiUse(details) => {
details.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "mandate_data.mandate_type.multi_use",
})?
@ -593,8 +766,8 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPay
)),
}?;
let request = Self::try_from(NuveiPaymentRequestData {
amount: item.request.amount.clone().to_string(),
currency: item.request.currency.clone().to_string(),
amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
currency: item.request.currency,
connector_auth_type: item.connector_auth_type.clone(),
client_request_id: item.attempt_id.clone(),
session_token: data.1,
@ -640,7 +813,7 @@ impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentsRequest {
merchant_site_id,
client_request_id,
request.amount.clone(),
request.currency.clone(),
request.currency.clone().to_string(),
time_stamp,
merchant_secret,
])?,
@ -673,7 +846,7 @@ impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentFlowRequest {
merchant_site_id,
client_request_id,
request.amount.clone(),
request.currency.clone(),
request.currency.clone().to_string(),
request.related_transaction_id.clone().unwrap_or_default(),
time_stamp,
merchant_secret,
@ -685,11 +858,10 @@ impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentFlowRequest {
}
}
/// Common request handler for all the flows that has below fields in common
#[derive(Debug, Clone, Default)]
pub struct NuveiPaymentRequestData {
pub amount: String,
pub currency: String,
pub currency: storage_models::enums::Currency,
pub related_transaction_id: Option<String>,
pub client_request_id: String,
pub connector_auth_type: types::ConnectorAuthType,
@ -704,7 +876,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NuveiPaymentFlowRequest {
client_request_id: item.attempt_id.clone(),
connector_auth_type: item.connector_auth_type.clone(),
amount: item.request.amount_to_capture.to_string(),
currency: item.request.currency.to_string(),
currency: item.request.currency,
related_transaction_id: Some(item.request.connector_transaction_id.clone()),
..Default::default()
})
@ -717,7 +889,7 @@ impl TryFrom<&types::RefundExecuteRouterData> for NuveiPaymentFlowRequest {
client_request_id: item.attempt_id.clone(),
connector_auth_type: item.connector_auth_type.clone(),
amount: item.request.amount.to_string(),
currency: item.request.currency.to_string(),
currency: item.request.currency,
related_transaction_id: Some(item.request.connector_transaction_id.clone()),
..Default::default()
})
@ -741,7 +913,7 @@ impl TryFrom<&types::PaymentsCancelRouterData> for NuveiPaymentFlowRequest {
client_request_id: item.attempt_id.clone(),
connector_auth_type: item.connector_auth_type.clone(),
amount: item.request.get_amount()?.to_string(),
currency: item.request.get_currency()?.to_string(),
currency: item.request.get_currency()?,
related_transaction_id: Some(item.request.connector_transaction_id.clone()),
..Default::default()
})
@ -868,14 +1040,11 @@ fn get_payment_status(response: &NuveiPaymentsResponse) -> enums::AttemptStatus
NuveiTransactionStatus::Declined | NuveiTransactionStatus::Error => {
match response.transaction_type {
Some(NuveiTransactionType::Auth) => enums::AttemptStatus::AuthorizationFailed,
Some(NuveiTransactionType::Sale) | Some(NuveiTransactionType::Settle) => {
enums::AttemptStatus::Failure
}
Some(NuveiTransactionType::Void) => enums::AttemptStatus::VoidFailed,
Some(NuveiTransactionType::Auth3D) => {
enums::AttemptStatus::AuthenticationFailed
}
_ => enums::AttemptStatus::Pending,
_ => enums::AttemptStatus::Failure,
}
}
NuveiTransactionStatus::Processing => enums::AttemptStatus::Pending,
@ -888,16 +1057,58 @@ fn get_payment_status(response: &NuveiPaymentsResponse) -> enums::AttemptStatus
}
}
fn build_error_response<T>(
response: &NuveiPaymentsResponse,
http_code: u16,
) -> Option<Result<T, types::ErrorResponse>> {
match response.status {
NuveiPaymentStatus::Error => Some(get_error_response(
response.err_code,
&response.reason,
http_code,
)),
_ => {
let err = Some(get_error_response(
response.gw_error_code,
&response.gw_error_reason,
http_code,
));
match response.transaction_status {
Some(NuveiTransactionStatus::Error) => err,
_ => match response
.gw_error_reason
.as_ref()
.map(|r| r.eq("Missing argument"))
{
Some(true) => err,
_ => None,
},
}
}
}
}
pub trait NuveiPaymentsGenericResponse {}
impl NuveiPaymentsGenericResponse for api::Authorize {}
impl NuveiPaymentsGenericResponse for api::CompleteAuthorize {}
impl NuveiPaymentsGenericResponse for api::Void {}
impl NuveiPaymentsGenericResponse for api::PSync {}
impl NuveiPaymentsGenericResponse for api::Capture {}
impl<F, T>
TryFrom<types::ResponseRouterData<F, NuveiPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
where
F: NuveiPaymentsGenericResponse,
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, NuveiPaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let redirection_data = match item.data.payment_method {
storage_models::enums::PaymentMethod::Wallet => item
storage_models::enums::PaymentMethod::Wallet
| storage_models::enums::PaymentMethod::BankRedirect => item
.response
.payment_option
.as_ref()
@ -920,17 +1131,10 @@ impl<F, T>
let response = item.response;
Ok(Self {
status: get_payment_status(&response),
response: match response.status {
NuveiPaymentStatus::Error => {
get_error_response(response.err_code, response.reason, item.http_code)
}
_ => match response.transaction_status {
Some(NuveiTransactionStatus::Error) => get_error_response(
response.gw_error_code,
response.gw_error_reason,
item.http_code,
),
_ => Ok(types::PaymentsResponseData::TransactionResponse {
response: if let Some(err) = build_error_response(&response, item.http_code) {
err
} else {
Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: response
.transaction_id
.map_or(response.order_id, Some) // For paypal there will be no transaction_id, only order_id will be present
@ -952,9 +1156,35 @@ impl<F, T>
} else {
None
},
})
},
..item.data
})
}
}
impl TryFrom<types::PaymentsInitResponseRouterData<NuveiPaymentsResponse>>
for types::PaymentsInitRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::PaymentsInitResponseRouterData<NuveiPaymentsResponse>,
) -> Result<Self, Self::Error> {
let response = item.response;
let is_enrolled_for_3ds = response
.clone()
.payment_option
.and_then(|po| po.card)
.and_then(|c| c.three_d)
.and_then(|t| t.v2supported)
.map(utils::to_boolean)
.unwrap_or_default();
Ok(Self {
status: get_payment_status(&response),
response: Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse {
enrolled_v2: is_enrolled_for_3ds,
related_transaction_id: response.transaction_id,
}),
},
},
..item.data
})
}
@ -1022,11 +1252,11 @@ fn get_refund_response(
.unwrap_or(enums::RefundStatus::Failure);
match response.status {
NuveiPaymentStatus::Error => {
get_error_response(response.err_code, response.reason, http_code)
get_error_response(response.err_code, &response.reason, http_code)
}
_ => match response.transaction_status {
Some(NuveiTransactionStatus::Error) => {
get_error_response(response.gw_error_code, response.gw_error_reason, http_code)
get_error_response(response.gw_error_code, &response.gw_error_reason, http_code)
}
_ => Ok(types::RefundsResponseData {
connector_refund_id: txn_id,
@ -1038,14 +1268,16 @@ fn get_refund_response(
fn get_error_response<T>(
error_code: Option<i64>,
error_msg: Option<String>,
error_msg: &Option<String>,
http_code: u16,
) -> Result<T, types::ErrorResponse> {
Err(types::ErrorResponse {
code: error_code
.map(|c| c.to_string())
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()),
message: error_msg.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
message: error_msg
.clone()
.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
reason: None,
status_code: http_code,
})

View File

@ -117,8 +117,10 @@ impl types::PaymentsAuthorizeRouterData {
.execute_pretasks(self, state)
.await
.map_err(|error| error.to_payment_failed_response())?;
logger::debug!(completed_pre_tasks=?true);
if self.should_proceed_with_authorize() {
self.decide_authentication_type();
logger::debug!(auth_type=?self.auth_type);
let resp = services::execute_connector_processing_step(
state,
connector_integration,

View File

@ -387,6 +387,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
types::PaymentsResponseData::SessionResponse { .. } => (None, None),
types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None),
types::PaymentsResponseData::TokenizationResponse { .. } => (None, None),
types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None),
},
};

View File

@ -51,6 +51,8 @@ pub type PaymentsSyncResponseRouterData<R> =
ResponseRouterData<api::PSync, R, PaymentsSyncData, PaymentsResponseData>;
pub type PaymentsSessionResponseRouterData<R> =
ResponseRouterData<api::Session, R, PaymentsSessionData, PaymentsResponseData>;
pub type PaymentsInitResponseRouterData<R> =
ResponseRouterData<api::InitPayment, R, PaymentsAuthorizeData, PaymentsResponseData>;
pub type PaymentsCaptureResponseRouterData<R> =
ResponseRouterData<api::Capture, R, PaymentsCaptureData, PaymentsResponseData>;
pub type TokenizationResponseRouterData<R> = ResponseRouterData<
@ -283,6 +285,10 @@ pub enum PaymentsResponseData {
TokenizationResponse {
token: String,
},
ThreeDSEnrollmentResponse {
enrolled_v2: bool,
related_transaction_id: Option<String>,
},
}
#[derive(Debug, Clone, Default)]

View File

@ -18,11 +18,13 @@ mod mollie;
mod multisafepay;
mod nexinets;
mod nuvei;
mod nuvei_ui;
mod opennode;
mod payeezy;
mod paypal;
mod payu;
mod rapyd;
mod selenium;
mod shift4;
mod stripe;
mod trustpay;

View File

@ -0,0 +1,177 @@
use serial_test::serial;
use thirtyfour::{prelude::*, WebDriver};
use crate::{selenium::*, tester};
struct NuveiSeleniumTest;
impl SeleniumTest for NuveiSeleniumTest {}
async fn should_make_nuvei_3ds_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_redirection_payment(c, vec![
Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=pm-card&cname=CL-BRW1&ccnum=4000027891380961&expmonth=10&expyear=25&cvv=123&amount=200&country=US&currency=USD")),
Event::Assert(Assert::IsPresent("Exp Year")),
Event::Trigger(Trigger::Click(By::Id("card-submit-btn"))),
Event::Trigger(Trigger::Query(By::ClassName("title"))),
Event::Assert(Assert::Eq(Selector::Title, "ThreeDS ACS Emulator - Challenge Page")),
Event::Trigger(Trigger::Click(By::Id("btn1"))),
Event::Trigger(Trigger::Click(By::Id("btn5"))),
Event::Assert(Assert::IsPresent("Google")),
Event::Assert(Assert::Contains(Selector::QueryParamStr, "status=succeeded")),
]).await?;
Ok(())
}
async fn should_make_nuvei_3ds_mandate_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_redirection_payment(c, vec![
Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=pm-card&cname=CL-BRW1&ccnum=4000027891380961&expmonth=10&expyear=25&cvv=123&amount=200&country=US&currency=USD&setup_future_usage=off_session&mandate_data[customer_acceptance][acceptance_type]=offline&mandate_data[customer_acceptance][accepted_at]=1963-05-03T04:07:52.723Z&mandate_data[customer_acceptance][online][ip_address]=in%20sit&mandate_data[customer_acceptance][online][user_agent]=amet%20irure%20esse&mandate_data[mandate_type][multi_use][amount]=7000&mandate_data[mandate_type][multi_use][currency]=USD&mandate_data[mandate_type][multi_use][start_date]=2022-09-10T00:00:00Z&mandate_data[mandate_type][multi_use][end_date]=2023-09-10T00:00:00Z&mandate_data[mandate_type][multi_use][metadata][frequency]=13")),
Event::Trigger(Trigger::Click(By::Id("card-submit-btn"))),
Event::Trigger(Trigger::Query(By::ClassName("title"))),
Event::Assert(Assert::Eq(Selector::Title, "ThreeDS ACS Emulator - Challenge Page")),
Event::Trigger(Trigger::Click(By::Id("btn1"))),
Event::Trigger(Trigger::Click(By::Id("btn5"))),
Event::Assert(Assert::IsPresent("Google")),
Event::Assert(Assert::Contains(Selector::QueryParamStr, "status=succeeded")),
]).await?;
Ok(())
}
async fn should_make_nuvei_gpay_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_gpay_payment(c,
"https://hs-payment-tests.w3spaces.com?pay-mode=pm-gpay&gatewayname=nuveidigital&gatewaymerchantid=googletest&amount=10.00&country=IN&currency=USD",
vec![
Event::Assert(Assert::IsPresent("succeeded")),
]).await?;
Ok(())
}
async fn should_make_nuvei_pypl_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_paypal_payment(c,
"https://hs-payment-tests.w3spaces.com?pay-mode=pypl-redirect&amount=12.00&country=US&currency=USD",
vec![
Event::Assert(Assert::IsPresent("Your transaction has been successfully executed.")),
]).await?;
Ok(())
}
async fn should_make_nuvei_giropay_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_redirection_payment(c, vec![
Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=1.00&country=DE&currency=EUR&paymentmethod=giropay")),
Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
Event::Assert(Assert::IsPresent("You are about to make a payment using the Giropay service.")),
Event::Trigger(Trigger::Click(By::Id("ctl00_ctl00_mainContent_btnConfirm"))),
Event::RunIf(Assert::IsPresent("Bank suchen"), vec![
Event::Trigger(Trigger::SendKeys(By::Id("bankSearch"), "GIROPAY Testbank 1")),
Event::Trigger(Trigger::Click(By::Id("GIROPAY Testbank 1"))),
]),
Event::Assert(Assert::IsPresent("GIROPAY Testbank 1")),
Event::Trigger(Trigger::Click(By::Css("button[name='claimCheckoutButton']"))),
Event::Assert(Assert::IsPresent("sandbox.paydirekt")),
Event::Trigger(Trigger::Click(By::Id("submitButton"))),
Event::Trigger(Trigger::Sleep(5)),
Event::Trigger(Trigger::SwitchTab(Position::Next)),
Event::Assert(Assert::IsPresent("Sicher bezahlt!")),
Event::Assert(Assert::IsPresent("Your transaction")) // Transaction succeeds sometimes and pending sometimes
]).await?;
Ok(())
}
async fn should_make_nuvei_ideal_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_redirection_payment(c, vec![
Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=10.00&country=NL&currency=EUR&paymentmethod=ideal&processingbank=ing")),
Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
Event::Assert(Assert::IsPresent("Your account will be debited:")),
Event::Trigger(Trigger::SelectOption(By::Id("ctl00_ctl00_mainContent_ServiceContent_ddlBanks"), "ING Simulator")),
Event::Trigger(Trigger::Click(By::Id("ctl00_ctl00_mainContent_btnConfirm"))),
Event::Assert(Assert::IsPresent("IDEALFORTIS")),
Event::Trigger(Trigger::Sleep(5)),
Event::Trigger(Trigger::Click(By::Id("ctl00_mainContent_btnGo"))),
Event::Assert(Assert::IsPresent("Your transaction")),// Transaction succeeds sometimes and pending sometimes
]).await?;
Ok(())
}
async fn should_make_nuvei_sofort_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_redirection_payment(c, vec![
Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=10.00&country=DE&currency=EUR&paymentmethod=sofort")),
Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
Event::Assert(Assert::IsPresent("SOFORT")),
Event::Trigger(Trigger::ChangeQueryParam("sender_holder", "John Doe")),
Event::Trigger(Trigger::Click(By::Id("ctl00_mainContent_btnGo"))),
Event::Assert(Assert::IsPresent("Your transaction")),// Transaction succeeds sometimes and pending sometimes
]).await?;
Ok(())
}
async fn should_make_nuvei_eps_payment(c: WebDriver) -> Result<(), WebDriverError> {
let conn = NuveiSeleniumTest {};
conn.make_redirection_payment(c, vec![
Event::Trigger(Trigger::Goto("https://hs-payment-tests.w3spaces.com?pay-mode=bank-redirect&amount=10.00&country=AT&currency=EUR&paymentmethod=eps&processingbank=ing")),
Event::Trigger(Trigger::Click(By::Id("bank-redirect-btn"))),
Event::Assert(Assert::IsPresent("You are about to make a payment using the EPS service.")),
Event::Trigger(Trigger::SendKeys(By::Id("ctl00_ctl00_mainContent_ServiceContent_txtCustomerName"), "John Doe")),
Event::Trigger(Trigger::Click(By::Id("ctl00_ctl00_mainContent_btnConfirm"))),
Event::Assert(Assert::IsPresent("Simulator")),
Event::Trigger(Trigger::SelectOption(By::Css("select[name='result']"), "Succeeded")),
Event::Trigger(Trigger::Click(By::Id("submitbutton"))),
Event::Assert(Assert::IsPresent("Your transaction")),// Transaction succeeds sometimes and pending sometimes
]).await?;
Ok(())
}
#[test]
#[serial]
fn should_make_nuvei_3ds_payment_test() {
tester!(should_make_nuvei_3ds_payment, "firefox");
}
#[test]
#[serial]
fn should_make_nuvei_3ds_mandate_payment_test() {
tester!(should_make_nuvei_3ds_mandate_payment, "firefox");
}
#[test]
#[serial]
fn should_make_nuvei_gpay_payment_test() {
tester!(should_make_nuvei_gpay_payment, "firefox");
}
#[test]
#[serial]
fn should_make_nuvei_pypl_payment_test() {
tester!(should_make_nuvei_pypl_payment, "firefox");
}
#[test]
#[serial]
fn should_make_nuvei_giropay_payment_test() {
tester!(should_make_nuvei_giropay_payment, "firefox");
}
#[test]
#[serial]
fn should_make_nuvei_ideal_payment_test() {
tester!(should_make_nuvei_ideal_payment, "firefox");
}
#[test]
#[serial]
fn should_make_nuvei_sofort_payment_test() {
tester!(should_make_nuvei_sofort_payment, "firefox");
}
#[test]
#[serial]
fn should_make_nuvei_eps_payment_test() {
tester!(should_make_nuvei_eps_payment, "firefox");
}

View File

@ -0,0 +1,469 @@
use std::{collections::HashMap, env, path::MAIN_SEPARATOR, time::Duration};
use actix_web::cookie::SameSite;
use async_trait::async_trait;
use futures::Future;
use thirtyfour::{components::SelectElement, prelude::*, WebDriver};
pub enum Event<'a> {
RunIf(Assert<'a>, Vec<Event<'a>>),
EitherOr(Assert<'a>, Vec<Event<'a>>, Vec<Event<'a>>),
Assert(Assert<'a>),
Trigger(Trigger<'a>),
}
#[allow(dead_code)]
pub enum Trigger<'a> {
Goto(&'a str),
Click(By),
ClickNth(By, usize),
SelectOption(By, &'a str),
ChangeQueryParam(&'a str, &'a str),
SwitchTab(Position),
SwitchFrame(By),
Find(By),
Query(By),
SendKeys(By, &'a str),
Sleep(u64),
}
pub enum Position {
Prev,
Next,
}
pub enum Selector {
Title,
QueryParamStr,
}
pub enum Assert<'a> {
Eq(Selector, &'a str),
Contains(Selector, &'a str),
IsPresent(&'a str),
}
#[async_trait]
pub trait SeleniumTest {
async fn complete_actions(
&self,
driver: &WebDriver,
actions: Vec<Event<'_>>,
) -> Result<(), WebDriverError> {
for action in actions {
match action {
Event::Assert(assert) => match assert {
Assert::Contains(selector, text) => match selector {
Selector::QueryParamStr => {
let url = driver.current_url().await?;
assert!(url.query().unwrap().contains(text))
}
_ => assert!(driver.title().await?.contains(text)),
},
Assert::Eq(_selector, text) => assert_eq!(driver.title().await?, text),
Assert::IsPresent(text) => {
assert!(is_text_present(driver, text).await?)
}
},
Event::RunIf(con_event, events) => match con_event {
Assert::Contains(selector, text) => match selector {
Selector::QueryParamStr => {
let url = driver.current_url().await?;
if url.query().unwrap().contains(text) {
self.complete_actions(driver, events).await?;
}
}
_ => assert!(driver.title().await?.contains(text)),
},
Assert::Eq(_selector, text) => {
if text == driver.title().await? {
self.complete_actions(driver, events).await?;
}
}
Assert::IsPresent(text) => {
if is_text_present(driver, text).await.is_ok() {
self.complete_actions(driver, events).await?;
}
}
},
Event::EitherOr(con_event, success, failure) => match con_event {
Assert::Contains(selector, text) => match selector {
Selector::QueryParamStr => {
let url = driver.current_url().await?;
self.complete_actions(
driver,
if url.query().unwrap().contains(text) {
success
} else {
failure
},
)
.await?;
}
_ => assert!(driver.title().await?.contains(text)),
},
Assert::Eq(_selector, text) => {
self.complete_actions(
driver,
if text == driver.title().await? {
success
} else {
failure
},
)
.await?;
}
Assert::IsPresent(text) => {
self.complete_actions(
driver,
if is_text_present(driver, text).await.is_ok() {
success
} else {
failure
},
)
.await?;
}
},
Event::Trigger(trigger) => match trigger {
Trigger::Goto(url) => {
driver.goto(url).await?;
let hs_base_url =
env::var("HS_BASE_URL").unwrap_or("http://localhost:8080".to_string()); //Issue: #924
let hs_api_key =
env::var("HS_API_KEY").expect("Hyperswitch user API key not present"); //Issue: #924
driver
.add_cookie(new_cookie("hs_base_url", hs_base_url).clone())
.await?;
driver
.add_cookie(new_cookie("hs_api_key", hs_api_key).clone())
.await?;
}
Trigger::Click(by) => {
let ele = driver.query(by).first().await?;
ele.wait_until().displayed().await?;
ele.wait_until().clickable().await?;
ele.click().await?;
}
Trigger::ClickNth(by, n) => {
let ele = driver.query(by).all().await?.into_iter().nth(n).unwrap();
ele.wait_until().displayed().await?;
ele.wait_until().clickable().await?;
ele.click().await?;
}
Trigger::Find(by) => {
driver.find(by).await?;
}
Trigger::Query(by) => {
driver.query(by).first().await?;
}
Trigger::SendKeys(by, input) => {
let ele = driver.query(by).first().await?;
ele.wait_until().displayed().await?;
ele.send_keys(&input).await?;
}
Trigger::SelectOption(by, input) => {
let ele = driver.query(by).first().await?;
let select_element = SelectElement::new(&ele).await?;
select_element.select_by_partial_text(input).await?;
}
Trigger::ChangeQueryParam(param, value) => {
let mut url = driver.current_url().await?;
let mut hash_query: HashMap<String, String> =
url.query_pairs().into_owned().collect();
hash_query.insert(param.to_string(), value.to_string());
let url_str = serde_urlencoded::to_string(hash_query)
.expect("Query Param update failed");
url.set_query(Some(&url_str));
driver.goto(url.as_str()).await?;
}
Trigger::Sleep(seconds) => {
tokio::time::sleep(Duration::from_secs(seconds)).await;
}
Trigger::SwitchTab(position) => match position {
Position::Next => {
let windows = driver.windows().await?;
if let Some(window) = windows.iter().rev().next() {
driver.switch_to_window(window.to_owned()).await?;
}
}
Position::Prev => {
let windows = driver.windows().await?;
if let Some(window) = windows.into_iter().next() {
driver.switch_to_window(window.to_owned()).await?;
}
}
},
Trigger::SwitchFrame(by) => {
let iframe = driver.query(by).first().await?;
iframe.wait_until().displayed().await?;
iframe.clone().enter_frame().await?;
}
},
}
}
Ok(())
}
async fn process_payment<F, Fut>(&self, _f: F) -> Result<(), WebDriverError>
where
F: FnOnce(WebDriver) -> Fut + Send,
Fut: Future<Output = Result<(), WebDriverError>> + Send,
{
let _browser = env::var("HS_TEST_BROWSER").unwrap_or("chrome".to_string()); //Issue: #924
Ok(())
}
async fn make_redirection_payment(
&self,
c: WebDriver,
actions: Vec<Event<'_>>,
) -> Result<(), WebDriverError> {
self.complete_actions(&c, actions).await
}
async fn make_gpay_payment(
&self,
c: WebDriver,
url: &str,
actions: Vec<Event<'_>>,
) -> Result<(), WebDriverError> {
let (email, pass) = (
&get_env("GMAIL_EMAIL").clone(),
&get_env("GMAIL_PASS").clone(),
);
let default_actions = vec![
Event::Trigger(Trigger::Goto(url)),
Event::Trigger(Trigger::Click(By::Css("#gpay-btn button"))),
Event::Trigger(Trigger::SwitchTab(Position::Next)),
Event::RunIf(
Assert::IsPresent("Sign in"),
vec![
Event::Trigger(Trigger::SendKeys(By::Id("identifierId"), email)),
Event::Trigger(Trigger::ClickNth(By::Tag("button"), 2)),
Event::EitherOr(
Assert::IsPresent("Welcome"),
vec![
Event::Trigger(Trigger::SendKeys(By::Name("Passwd"), pass)),
Event::Trigger(Trigger::Sleep(2)),
Event::Trigger(Trigger::Click(By::Id("passwordNext"))),
],
vec![
Event::Trigger(Trigger::SendKeys(By::Id("identifierId"), email)),
Event::Trigger(Trigger::ClickNth(By::Tag("button"), 2)),
Event::Trigger(Trigger::SendKeys(By::Name("Passwd"), pass)),
Event::Trigger(Trigger::Sleep(2)),
Event::Trigger(Trigger::Click(By::Id("passwordNext"))),
],
),
],
),
Event::Trigger(Trigger::Query(By::ClassName(
"bootstrapperIframeContainerElement",
))),
Event::Trigger(Trigger::SwitchFrame(By::Id("sM432dIframe"))),
Event::Assert(Assert::IsPresent("Gpay Tester")),
Event::Trigger(Trigger::Click(By::ClassName("jfk-button-action"))),
Event::Trigger(Trigger::SwitchTab(Position::Prev)),
];
self.complete_actions(&c, default_actions).await?;
self.complete_actions(&c, actions).await
}
async fn make_paypal_payment(
&self,
c: WebDriver,
url: &str,
actions: Vec<Event<'_>>,
) -> Result<(), WebDriverError> {
self.complete_actions(
&c,
vec![
Event::Trigger(Trigger::Goto(url)),
Event::Trigger(Trigger::Click(By::Id("pypl-redirect-btn"))),
],
)
.await?;
let (email, pass) = (
&get_env("PYPL_EMAIL").clone(),
&get_env("PYPL_PASS").clone(),
);
let mut pypl_actions = vec![
Event::EitherOr(
Assert::IsPresent("Password"),
vec![
Event::Trigger(Trigger::SendKeys(By::Id("password"), pass)),
Event::Trigger(Trigger::Click(By::Id("btnLogin"))),
],
vec![
Event::Trigger(Trigger::SendKeys(By::Id("email"), email)),
Event::Trigger(Trigger::Click(By::Id("btnNext"))),
Event::Trigger(Trigger::SendKeys(By::Id("password"), pass)),
Event::Trigger(Trigger::Click(By::Id("btnLogin"))),
],
),
Event::Trigger(Trigger::Click(By::Id("payment-submit-btn"))),
];
pypl_actions.extend(actions);
self.complete_actions(&c, pypl_actions).await
}
}
async fn is_text_present(driver: &WebDriver, key: &str) -> WebDriverResult<bool> {
let mut xpath = "//*[contains(text(),'".to_owned();
xpath.push_str(key);
xpath.push_str("')]");
let result = driver.query(By::XPath(&xpath)).first().await?;
result.is_present().await
}
fn new_cookie(name: &str, value: String) -> Cookie<'_> {
let mut base_url_cookie = Cookie::new(name, value);
base_url_cookie.set_same_site(Some(SameSite::Lax));
base_url_cookie.set_domain("hs-payment-tests.w3spaces.com");
base_url_cookie.set_path("/");
base_url_cookie
}
#[macro_export]
macro_rules! tester_inner {
($execute:ident, $webdriver:expr) => {{
use std::{
sync::{Arc, Mutex},
thread,
};
let driver = $webdriver;
// we'll need the session_id from the thread
// NOTE: even if it panics, so can't just return it
let session_id = Arc::new(Mutex::new(None));
// run test in its own thread to catch panics
let sid = session_id.clone();
let res = thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let driver = runtime
.block_on(driver)
.expect("failed to construct test WebDriver");
*sid.lock().unwrap() = runtime.block_on(driver.session_id()).ok();
// make sure we close, even if an assertion fails
let client = driver.clone();
let x = runtime.block_on(async move {
let r = tokio::spawn($execute(driver)).await;
let _ = client.quit().await;
r
});
drop(runtime);
x.expect("test panicked")
})
.join();
let success = handle_test_error(res);
assert!(success);
}};
}
#[macro_export]
macro_rules! tester {
($f:ident, $endpoint:expr) => {{
use $crate::tester_inner;
let url = make_url($endpoint);
let caps = make_capabilities($endpoint);
tester_inner!($f, WebDriver::new(url, caps));
}};
}
pub fn make_capabilities(s: &str) -> Capabilities {
match s {
"firefox" => {
let mut caps = DesiredCapabilities::firefox();
let profile_path = &format!("-profile={}", get_firefox_profile_path().unwrap());
caps.add_firefox_arg(profile_path).unwrap();
// let mut prefs = FirefoxPreferences::new();
// prefs.set("-browser.link.open_newwindow", 3).unwrap();
// caps.set_preferences(prefs).unwrap();
caps.into()
}
"chrome" => {
let mut caps = DesiredCapabilities::chrome();
let profile_path = &format!("user-data-dir={}", get_chrome_profile_path().unwrap());
caps.add_chrome_arg(profile_path).unwrap();
// caps.set_headless().unwrap();
// caps.set_no_sandbox().unwrap();
// caps.set_disable_gpu().unwrap();
// caps.set_disable_dev_shm_usage().unwrap();
caps.into()
}
&_ => DesiredCapabilities::safari().into(),
}
}
fn get_chrome_profile_path() -> Result<String, WebDriverError> {
env::var("CHROME_PROFILE_PATH").map_or_else(
//Issue: #924
|_| -> Result<String, WebDriverError> {
let exe = env::current_exe()?;
let dir = exe.parent().expect("Executable must be in some directory");
let mut base_path = dir
.to_str()
.map(|str| {
let mut fp = str.split(MAIN_SEPARATOR).collect::<Vec<_>>();
fp.truncate(3);
fp.join(&MAIN_SEPARATOR.to_string())
})
.unwrap();
base_path.push_str(r#"/Library/Application\ Support/Google/Chrome/Default"#);
Ok(base_path)
},
Ok,
)
}
fn get_firefox_profile_path() -> Result<String, WebDriverError> {
env::var("FIREFOX_PROFILE_PATH").map_or_else(
//Issue: #924
|_| -> Result<String, WebDriverError> {
let exe = env::current_exe()?;
let dir = exe.parent().expect("Executable must be in some directory");
let mut base_path = dir
.to_str()
.map(|str| {
let mut fp = str.split(MAIN_SEPARATOR).collect::<Vec<_>>();
fp.truncate(3);
fp.join(&MAIN_SEPARATOR.to_string())
})
.unwrap();
base_path.push_str(r#"/Library/Application Support/Firefox/Profiles/hs-test"#);
Ok(base_path)
},
Ok,
)
}
pub fn make_url(s: &str) -> &'static str {
match s {
"firefox" => "http://localhost:4444",
"chrome" => "http://localhost:9515",
&_ => "",
}
}
pub fn handle_test_error(
res: Result<Result<(), WebDriverError>, Box<dyn std::any::Any + Send>>,
) -> bool {
match res {
Ok(Ok(_)) => true,
Ok(Err(e)) => {
eprintln!("test future failed to resolve: {:?}", e);
false
}
Err(e) => {
if let Some(e) = e.downcast_ref::<WebDriverError>() {
eprintln!("test future panicked: {:?}", e);
} else {
eprintln!("test future panicked; an assertion probably failed");
}
false
}
}
}
pub fn get_env(name: &str) -> String {
env::var(name).unwrap_or_else(|_| panic!("{name} not present")) //Issue: #924
}

View File

@ -400,6 +400,7 @@ pub trait ConnectorActions: Connector {
Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None,
Ok(types::PaymentsResponseData::TokenizationResponse { .. }) => None,
Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None,
Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None,
Err(_) => None,
}
}
@ -581,6 +582,7 @@ pub fn get_connector_transaction_id(
Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None,
Ok(types::PaymentsResponseData::TokenizationResponse { .. }) => None,
Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None,
Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None,
Err(_) => None,
}
}