feat(router): send 2xx payments response for all the connector http responses (2xx, 4xx etc.) (#1924)

This commit is contained in:
Sai Harsha Vardhan
2023-08-17 15:26:07 +05:30
committed by GitHub
parent 35963e279a
commit 0ab6827f6c
18 changed files with 314 additions and 223 deletions

View File

@ -71,6 +71,28 @@ where
),
}
}
Ok(api::ApplicationResponse::JsonWithHeaders((response, headers))) => {
let response = S::try_from(response);
match response {
Ok(response) => match serde_json::to_string(&response) {
Ok(res) => api::http_response_json_with_headers(res, headers),
Err(_) => api::http_response_err(
r#"{
"error": {
"message": "Error serializing response from connector"
}
}"#,
),
},
Err(_) => api::http_response_err(
r#"{
"error": {
"message": "Error converting juspay response to stripe response"
}
}"#,
),
}
}
Ok(api::ApplicationResponse::StatusOk) => api::http_response_ok(),
Ok(api::ApplicationResponse::TextPlain(text)) => api::http_response_plaintext(text),
Ok(api::ApplicationResponse::FileData((file_data, content_type))) => {

View File

@ -39,7 +39,7 @@ use crate::{
self, api, domain,
storage::{self, enums as storage_enums, ProcessTrackerExt},
},
utils::{Encode, OptionExt, ValueExt},
utils::{add_connector_http_status_code_metrics, Encode, OptionExt, ValueExt},
};
#[instrument(skip_all, fields(payment_id, merchant_id))]
@ -51,7 +51,7 @@ pub async fn payments_operation_core<F, Req, Op, FData>(
req: Req,
call_connector_action: CallConnectorAction,
auth_flow: services::AuthFlow,
) -> RouterResult<(PaymentData<F>, Req, Option<domain::Customer>)>
) -> RouterResult<(PaymentData<F>, Req, Option<domain::Customer>, Option<u16>)>
where
F: Send + Clone + Sync,
Req: Authenticate,
@ -151,6 +151,8 @@ where
)
.await?;
let mut connector_http_status_code = None;
if let Some(connector_details) = connector {
payment_data = match connector_details {
api::ConnectorCallType::Single(connector) => {
@ -172,6 +174,9 @@ where
let operation = Box::new(PaymentResponse);
let db = &*state.store;
connector_http_status_code = router_data.connector_http_status_code;
//add connector http status code metrics
add_connector_http_status_code_metrics(connector_http_status_code);
operation
.to_post_update_tracker()?
.update_tracker(
@ -226,7 +231,7 @@ where
.await?;
}
Ok((payment_data, req, customer))
Ok((payment_data, req, customer, connector_http_status_code))
}
#[allow(clippy::too_many_arguments)]
@ -256,7 +261,7 @@ where
// To perform router related operation for PaymentResponse
PaymentResponse: Operation<F, FData>,
{
let (payment_data, req, customer) = payments_operation_core(
let (payment_data, req, customer, connector_http_status_code) = payments_operation_core(
state,
merchant_account,
key_store,
@ -275,6 +280,7 @@ where
&state.conf.server,
operation,
&state.conf.connector_request_reference_id_config,
connector_http_status_code,
)
}
@ -373,6 +379,7 @@ pub trait PaymentRedirectFlow: Sync {
let payments_response = match response? {
services::ApplicationResponse::Json(response) => Ok(response),
services::ApplicationResponse::JsonWithHeaders((response, _)) => Ok(response),
_ => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Failed to get the response in json"),

View File

@ -2462,6 +2462,7 @@ pub fn router_data_type_conversion<F1, F2, Req1, Req2, Res1, Res2>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode: router_data.test_mode,
connector_http_status_code: router_data.connector_http_status_code,
}
}

View File

@ -1,5 +1,4 @@
use async_trait::async_trait;
use common_utils::fp_utils;
use error_stack::ResultExt;
use router_derive;
@ -51,9 +50,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsAuthorizeData
.mandate_id
.or_else(|| router_data.request.mandate_id.clone());
let router_response = router_data.response.clone();
let connector = router_data.connector.clone();
payment_data = payment_response_update_tracker(
db,
payment_id,
@ -63,22 +59,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsAuthorizeData
)
.await?;
router_response.map(|_| ()).or_else(|error_response| {
fp_utils::when(
!(200..300).contains(&error_response.status_code)
&& !(500..=511).contains(&error_response.status_code),
|| {
Err(errors::ApiErrorResponse::ExternalConnectorError {
code: error_response.code,
message: error_response.message,
connector,
status_code: error_response.status_code,
reason: error_response.reason,
})
},
)
})?;
Ok(payment_data)
}
}
@ -116,9 +96,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsSessionData>
where
F: 'b + Send,
{
let router_response = router_data.response.clone();
let connector = router_data.connector.clone();
payment_data = payment_response_update_tracker(
db,
payment_id,
@ -128,16 +105,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsSessionData>
)
.await?;
router_response.map_err(|error_response| {
errors::ApiErrorResponse::ExternalConnectorError {
message: error_response.message,
code: error_response.code,
status_code: error_response.status_code,
reason: error_response.reason,
connector,
}
})?;
Ok(payment_data)
}
}
@ -157,9 +124,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsCaptureData>
where
F: 'b + Send,
{
let router_response = router_data.response.clone();
let connector = router_data.connector.clone();
payment_data = payment_response_update_tracker(
db,
payment_id,
@ -169,16 +133,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsCaptureData>
)
.await?;
router_response.map_err(|error_response| {
errors::ApiErrorResponse::ExternalConnectorError {
message: error_response.message,
code: error_response.code,
status_code: error_response.status_code,
reason: error_response.reason,
connector,
}
})?;
Ok(payment_data)
}
}
@ -197,9 +151,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsCancelData> f
where
F: 'b + Send,
{
let router_response = router_data.response.clone();
let connector = router_data.connector.clone();
payment_data = payment_response_update_tracker(
db,
payment_id,
@ -209,16 +160,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::PaymentsCancelData> f
)
.await?;
router_response.map_err(|error_response| {
errors::ApiErrorResponse::ExternalConnectorError {
message: error_response.message,
code: error_response.code,
status_code: error_response.status_code,
reason: error_response.reason,
connector,
}
})?;
Ok(payment_data)
}
}
@ -242,9 +183,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::VerifyRequestData> fo
// .map(api_models::payments::MandateIds::new)
});
let router_response = router_data.response.clone();
let connector = router_data.connector.clone();
payment_data = payment_response_update_tracker(
db,
payment_id,
@ -254,16 +192,6 @@ impl<F: Clone> PostUpdateTracker<F, PaymentData<F>, types::VerifyRequestData> fo
)
.await?;
router_response.map_err(|error_response| {
errors::ApiErrorResponse::ExternalConnectorError {
message: error_response.message,
code: error_response.code,
status_code: error_response.status_code,
reason: error_response.reason,
connector,
}
})?;
Ok(payment_data)
}
}

View File

@ -138,6 +138,7 @@ where
quote_id: None,
test_mode,
payment_method_balance: None,
connector_http_status_code: None,
};
Ok(router_data)
@ -148,6 +149,7 @@ where
Self: Sized,
Op: Debug,
{
#[allow(clippy::too_many_arguments)]
fn generate_response(
req: Option<Req>,
data: D,
@ -156,6 +158,7 @@ where
server: &Server,
operation: Op,
connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
connector_http_status_code: Option<u16>,
) -> RouterResponse<Self>;
}
@ -164,6 +167,7 @@ where
F: Clone,
Op: Debug,
{
#[allow(clippy::too_many_arguments)]
fn generate_response(
req: Option<Req>,
payment_data: PaymentData<F>,
@ -172,6 +176,7 @@ where
server: &Server,
operation: Op,
connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
connector_http_status_code: Option<u16>,
) -> RouterResponse<Self> {
payments_to_payments_response(
req,
@ -192,6 +197,7 @@ where
payment_data.frm_message,
payment_data.setup_mandate,
connector_request_reference_id_config,
connector_http_status_code,
)
}
}
@ -202,6 +208,7 @@ where
F: Clone,
Op: Debug,
{
#[allow(clippy::too_many_arguments)]
fn generate_response(
_req: Option<Req>,
payment_data: PaymentData<F>,
@ -210,8 +217,10 @@ where
_server: &Server,
_operation: Op,
_connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
_connector_http_status_code: Option<u16>,
) -> RouterResponse<Self> {
Ok(services::ApplicationResponse::Json(Self {
Ok(services::ApplicationResponse::JsonWithHeaders((
Self {
session_token: payment_data.sessions_token,
payment_id: payment_data.payment_attempt.payment_id,
client_secret: payment_data
@ -219,7 +228,9 @@ where
.client_secret
.get_required_value("client_secret")?
.into(),
}))
},
vec![],
)))
}
}
@ -229,6 +240,7 @@ where
F: Clone,
Op: Debug,
{
#[allow(clippy::too_many_arguments)]
fn generate_response(
_req: Option<Req>,
data: PaymentData<F>,
@ -237,6 +249,7 @@ where
_server: &Server,
_operation: Op,
_connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
_connector_http_status_code: Option<u16>,
) -> RouterResponse<Self> {
let additional_payment_method_data: Option<api_models::payments::AdditionalPaymentData> =
data.payment_attempt
@ -249,7 +262,8 @@ where
})?;
let payment_method_data_response =
additional_payment_method_data.map(api::PaymentMethodDataResponse::from);
Ok(services::ApplicationResponse::Json(Self {
Ok(services::ApplicationResponse::JsonWithHeaders((
Self {
verify_id: Some(data.payment_intent.payment_id),
merchant_id: Some(data.payment_intent.merchant_id),
client_secret: data.payment_intent.client_secret.map(masking::Secret::new),
@ -269,7 +283,9 @@ where
payment_token: data.token,
error_code: data.payment_attempt.error_code,
error_message: data.payment_attempt.error_message,
}))
},
vec![],
)))
}
}
@ -296,6 +312,7 @@ pub fn payments_to_payments_response<R, Op>(
frm_message: Option<payments::FrmMessage>,
mandate_data: Option<api_models::payments::MandateData>,
connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
connector_http_status_code: Option<u16>,
) -> RouterResponse<api::PaymentsResponse>
where
Op: Debug,
@ -356,6 +373,15 @@ where
let payment_method_data_response =
additional_payment_method_data.map(api::PaymentMethodDataResponse::from);
let headers = connector_http_status_code
.map(|status_code| {
vec![(
"connector_http_status_code".to_string(),
status_code.to_string(),
)]
})
.unwrap_or(vec![]);
let output = Ok(match payment_request {
Some(_request) => {
if payments::is_start_pay(&operation) && redirection_data.is_some() {
@ -445,7 +471,7 @@ where
let amount_captured = payment_intent.amount_captured.unwrap_or_default();
let amount_capturable = Some(payment_attempt.amount - amount_captured);
services::ApplicationResponse::Json(
services::ApplicationResponse::JsonWithHeaders((
response
.set_payment_id(Some(payment_attempt.payment_id))
.set_merchant_id(Some(payment_attempt.merchant_id))
@ -531,10 +557,12 @@ where
.set_connector_metadata(payment_intent.connector_metadata)
.set_reference_id(payment_attempt.connector_response_reference_id)
.to_owned(),
)
headers,
))
}
}
None => services::ApplicationResponse::Json(api::PaymentsResponse {
None => services::ApplicationResponse::JsonWithHeaders((
api::PaymentsResponse {
payment_id: Some(payment_attempt.payment_id),
merchant_id: Some(payment_attempt.merchant_id),
status: payment_intent.status,
@ -585,7 +613,9 @@ where
allowed_payment_method_types: payment_intent.allowed_payment_method_types,
reference_id: payment_attempt.connector_response_reference_id,
..Default::default()
}),
},
headers,
)),
});
metrics::PAYMENT_OPS_COUNT.add(

View File

@ -182,6 +182,7 @@ pub async fn construct_payout_router_data<'a, F>(
quote_id: None,
test_mode,
payment_method_balance: None,
connector_http_status_code: None,
};
Ok(router_data)
@ -289,6 +290,7 @@ pub async fn construct_refund_router_data<'a, F>(
quote_id: None,
test_mode,
payment_method_balance: None,
connector_http_status_code: None,
};
Ok(router_data)
@ -506,6 +508,7 @@ pub async fn construct_accept_dispute_router_data<'a>(
quote_id: None,
test_mode,
payment_method_balance: None,
connector_http_status_code: None,
};
Ok(router_data)
}
@ -580,6 +583,7 @@ pub async fn construct_submit_evidence_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_http_status_code: None,
};
Ok(router_data)
}
@ -655,6 +659,7 @@ pub async fn construct_upload_file_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_http_status_code: None,
};
Ok(router_data)
}
@ -732,6 +737,7 @@ pub async fn construct_defend_dispute_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_http_status_code: None,
};
Ok(router_data)
}
@ -804,6 +810,7 @@ pub async fn construct_retrieve_file_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_http_status_code: None,
};
Ok(router_data)
}

View File

@ -93,7 +93,7 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
};
match payments_response {
services::ApplicationResponse::Json(payments_response) => {
services::ApplicationResponse::JsonWithHeaders((payments_response, _)) => {
let payment_id = payments_response
.payment_id
.clone()
@ -449,7 +449,7 @@ async fn bank_transfer_webhook_flow<W: types::OutgoingWebhookType>(
};
match response? {
services::ApplicationResponse::Json(payments_response) => {
services::ApplicationResponse::JsonWithHeaders((payments_response, _)) => {
let payment_id = payments_response
.payment_id
.clone()

View File

@ -70,6 +70,12 @@ counter_metric!(REDIRECTION_TRIGGERED, GLOBAL_METER);
// Connector Level Metric
counter_metric!(REQUEST_BUILD_FAILURE, GLOBAL_METER);
counter_metric!(UNIMPLEMENTED_FLOW, GLOBAL_METER);
// Connector http status code metrics
counter_metric!(CONNECTOR_HTTP_STATUS_CODE_1XX_COUNT, GLOBAL_METER);
counter_metric!(CONNECTOR_HTTP_STATUS_CODE_2XX_COUNT, GLOBAL_METER);
counter_metric!(CONNECTOR_HTTP_STATUS_CODE_3XX_COUNT, GLOBAL_METER);
counter_metric!(CONNECTOR_HTTP_STATUS_CODE_4XX_COUNT, GLOBAL_METER);
counter_metric!(CONNECTOR_HTTP_STATUS_CODE_5XX_COUNT, GLOBAL_METER);
// Service Level
counter_metric!(CARD_LOCKER_FAILURES, GLOBAL_METER);

View File

@ -57,7 +57,8 @@ pub fn track_response_status_code<Q>(response: &ApplicationResponse<Q>) -> i64 {
| ApplicationResponse::StatusOk
| ApplicationResponse::TextPlain(_)
| ApplicationResponse::Form(_)
| ApplicationResponse::FileData(_) => 200,
| ApplicationResponse::FileData(_)
| ApplicationResponse::JsonWithHeaders(_) => 200,
ApplicationResponse::JsonForRedirection(_) => 302,
}
}

View File

@ -50,7 +50,8 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow {
)
.await?;
let (payment_data, _, _) = payment_flows::payments_operation_core::<api::PSync, _, _, _>(
let (payment_data, _, _, _) =
payment_flows::payments_operation_core::<api::PSync, _, _, _>(
state,
merchant_account.clone(),
key_store,

View File

@ -285,7 +285,9 @@ where
match response {
Ok(body) => {
let response = match body {
Ok(body) => connector_integration
Ok(body) => {
let connector_http_status_code = Some(body.status_code);
let mut data = connector_integration
.handle_response(req, body)
.map_err(|error| {
if error.current_context()
@ -301,8 +303,12 @@ where
)
}
error
})?,
})?;
data.connector_http_status_code = connector_http_status_code;
data
}
Err(body) => {
router_data.connector_http_status_code = Some(body.status_code);
metrics::CONNECTOR_ERROR_RESPONSE_COUNT.add(
&metrics::CONTEXT,
1,
@ -528,6 +534,7 @@ pub enum ApplicationResponse<R> {
JsonForRedirection(api::RedirectionResponse),
Form(Box<RedirectionFormData>),
FileData((Vec<u8>, mime::Mime)),
JsonWithHeaders((R, Vec<(String, String)>)),
}
#[derive(Debug, Eq, PartialEq)]
@ -704,6 +711,18 @@ where
)
.respond_to(request)
.map_into_boxed_body(),
Ok(ApplicationResponse::JsonWithHeaders((response, headers))) => {
match serde_json::to_string(&response) {
Ok(res) => http_response_json_with_headers(res, headers),
Err(_) => http_response_err(
r#"{
"error": {
"message": "Error serializing response from connector"
}
}"#,
),
}
}
Err(error) => log_and_return_error_response(error),
};
@ -769,6 +788,19 @@ pub fn http_response_json<T: body::MessageBody + 'static>(response: T) -> HttpRe
.body(response)
}
pub fn http_response_json_with_headers<T: body::MessageBody + 'static>(
response: T,
headers: Vec<(String, String)>,
) -> HttpResponse {
let mut response_builder = HttpResponse::Ok();
for (name, value) in headers {
response_builder.append_header((name, value));
}
response_builder
.content_type(mime::APPLICATION_JSON)
.body(response)
}
pub fn http_response_plaintext<T: body::MessageBody + 'static>(res: T) -> HttpResponse {
HttpResponse::Ok().content_type(mime::TEXT_PLAIN).body(res)
}

View File

@ -259,6 +259,7 @@ pub struct RouterData<Flow, Request, Response> {
pub quote_id: Option<String>,
pub test_mode: Option<bool>,
pub connector_http_status_code: Option<u16>,
}
#[derive(Debug, Clone)]
@ -919,6 +920,7 @@ impl<F1, F2, T1, T2> From<(&RouterData<F1, T1, PaymentsResponseData>, T2)>
quote_id: data.quote_id.clone(),
test_mode: data.test_mode,
payment_method_balance: data.payment_method_balance.clone(),
connector_http_status_code: data.connector_http_status_code,
}
}
}
@ -990,6 +992,7 @@ impl<F1, F2>
quote_id: data.quote_id.clone(),
test_mode: data.test_mode,
payment_method_balance: None,
connector_http_status_code: data.connector_http_status_code,
}
}
}

View File

@ -365,3 +365,45 @@ pub fn handle_json_response_deserialization_failure(
}
}
}
pub fn get_http_status_code_type(
status_code: u16,
) -> CustomResult<String, errors::ApiErrorResponse> {
let status_code_type = match status_code {
100..=199 => "1xx",
200..=299 => "2xx",
300..=399 => "3xx",
400..=499 => "4xx",
500..=599 => "5xx",
_ => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Invalid http status code")?,
};
Ok(status_code_type.to_string())
}
pub fn add_connector_http_status_code_metrics(option_status_code: Option<u16>) {
if let Some(status_code) = option_status_code {
let status_code_type = get_http_status_code_type(status_code).ok();
match status_code_type.as_deref() {
Some("1xx") => {
metrics::CONNECTOR_HTTP_STATUS_CODE_1XX_COUNT.add(&metrics::CONTEXT, 1, &[])
}
Some("2xx") => {
metrics::CONNECTOR_HTTP_STATUS_CODE_2XX_COUNT.add(&metrics::CONTEXT, 1, &[])
}
Some("3xx") => {
metrics::CONNECTOR_HTTP_STATUS_CODE_3XX_COUNT.add(&metrics::CONTEXT, 1, &[])
}
Some("4xx") => {
metrics::CONNECTOR_HTTP_STATUS_CODE_4XX_COUNT.add(&metrics::CONTEXT, 1, &[])
}
Some("5xx") => {
metrics::CONNECTOR_HTTP_STATUS_CODE_5XX_COUNT.add(&metrics::CONTEXT, 1, &[])
}
_ => logger::info!("Skip metrics as invalid http status code received from connector"),
};
} else {
logger::info!("Skip metrics as no http status code received from connector")
}
}

View File

@ -88,6 +88,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
quote_id: None,
test_mode: None,
payment_method_balance: None,
connector_http_status_code: None,
}
}
@ -140,6 +141,7 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
quote_id: None,
test_mode: None,
payment_method_balance: None,
connector_http_status_code: None,
}
}

View File

@ -504,6 +504,7 @@ pub trait ConnectorActions: Connector {
quote_id: None,
test_mode: None,
payment_method_balance: None,
connector_http_status_code: None,
}
}

View File

@ -352,7 +352,8 @@ async fn payments_create_core() {
mandate_id: None,
..Default::default()
};
let expected_response = services::ApplicationResponse::Json(expected_response);
let expected_response =
services::ApplicationResponse::JsonWithHeaders((expected_response, vec![]));
let actual_response =
payments::payments_core::<api::Authorize, api::PaymentsResponse, _, _, _>(
&state,
@ -498,7 +499,8 @@ async fn payments_create_core_adyen_no_redirect() {
..Default::default()
};
let expected_response = services::ApplicationResponse::Json(api::PaymentsResponse {
let expected_response = services::ApplicationResponse::JsonWithHeaders((
api::PaymentsResponse {
payment_id: Some(payment_id.clone()),
status: api_enums::IntentStatus::Processing,
amount: 6540,
@ -512,7 +514,9 @@ async fn payments_create_core_adyen_no_redirect() {
refunds: None,
mandate_id: None,
..Default::default()
});
},
vec![],
));
let actual_response =
payments::payments_core::<api::Authorize, api::PaymentsResponse, _, _, _>(
&state,

View File

@ -112,7 +112,8 @@ async fn payments_create_core() {
mandate_id: None,
..Default::default()
};
let expected_response = services::ApplicationResponse::Json(expected_response);
let expected_response =
services::ApplicationResponse::JsonWithHeaders((expected_response, vec![]));
let actual_response =
router::core::payments::payments_core::<api::Authorize, api::PaymentsResponse, _, _, _>(
&state,
@ -260,7 +261,8 @@ async fn payments_create_core_adyen_no_redirect() {
..Default::default()
};
let expected_response = services::ApplicationResponse::Json(api::PaymentsResponse {
let expected_response = services::ApplicationResponse::JsonWithHeaders((
api::PaymentsResponse {
payment_id: Some(payment_id.clone()),
status: api_enums::IntentStatus::Processing,
amount: 6540,
@ -274,7 +276,9 @@ async fn payments_create_core_adyen_no_redirect() {
refunds: None,
mandate_id: None,
..Default::default()
});
},
vec![],
));
let actual_response =
router::core::payments::payments_core::<api::Authorize, api::PaymentsResponse, _, _, _>(
&state,

View File

@ -54,7 +54,7 @@ async fn should_fail_recurring_payment_due_to_authentication(
Event::Assert(Assert::IsPresent("man_")),// mandate id starting with man_
Event::Trigger(Trigger::Click(By::Css("#pm-mandate-btn a"))),
Event::Trigger(Trigger::Click(By::Id("card-submit-btn"))),
Event::Assert(Assert::IsPresent("authentication_required: authentication_required")),
Event::Assert(Assert::IsPresent("Your card was declined. This transaction requires authentication.")),
]).await?;
Ok(())
}