Skip to content

Commit 28d02f9

Browse files
fix(connector): [braintree] add 3ds redirection error mapping and metadata validation (#2552)
1 parent efa5320 commit 28d02f9

File tree

5 files changed

+150
-71
lines changed

5 files changed

+150
-71
lines changed

crates/router/src/connector/braintree.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,10 +1496,45 @@ impl services::ConnectorRedirectResponse for Braintree {
14961496
fn get_flow_type(
14971497
&self,
14981498
_query_params: &str,
1499-
_json_payload: Option<serde_json::Value>,
1500-
_action: services::PaymentAction,
1499+
json_payload: Option<serde_json::Value>,
1500+
action: services::PaymentAction,
15011501
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
1502-
Ok(payments::CallConnectorAction::Trigger)
1502+
match action {
1503+
services::PaymentAction::PSync => match json_payload {
1504+
Some(payload) => {
1505+
let redirection_response:braintree_graphql_transformers::BraintreeRedirectionResponse = serde_json::from_value(payload)
1506+
.into_report()
1507+
.change_context(
1508+
errors::ConnectorError::MissingConnectorRedirectionPayload {
1509+
field_name: "redirection_response",
1510+
},
1511+
)?;
1512+
let braintree_payload =
1513+
serde_json::from_str::<
1514+
braintree_graphql_transformers::BraintreeThreeDsErrorResponse,
1515+
>(&redirection_response.authentication_response);
1516+
let (error_code, error_message) = match braintree_payload {
1517+
Ok(braintree_response_payload) => (
1518+
braintree_response_payload.code,
1519+
braintree_response_payload.message,
1520+
),
1521+
Err(_) => (
1522+
consts::NO_ERROR_CODE.to_string(),
1523+
redirection_response.authentication_response,
1524+
),
1525+
};
1526+
Ok(payments::CallConnectorAction::StatusUpdate {
1527+
status: enums::AttemptStatus::AuthenticationFailed,
1528+
error_code: Some(error_code),
1529+
error_message: Some(error_message),
1530+
})
1531+
}
1532+
None => Ok(payments::CallConnectorAction::Avoid),
1533+
},
1534+
services::PaymentAction::CompleteAuthorize => {
1535+
Ok(payments::CallConnectorAction::Trigger)
1536+
}
1537+
}
15031538
}
15041539
}
15051540

crates/router/src/connector/braintree/braintree_graphql_transformers.rs

Lines changed: 70 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use common_utils::pii;
12
use error_stack::{IntoReport, ResultExt};
23
use masking::{ExposeInterface, Secret};
34
use serde::{Deserialize, Serialize};
@@ -77,8 +78,19 @@ pub enum BraintreePaymentsRequest {
7778

7879
#[derive(Debug, Deserialize)]
7980
pub struct BraintreeMeta {
80-
merchant_account_id: Option<Secret<String>>,
81-
merchant_config_currency: Option<types::storage::enums::Currency>,
81+
merchant_account_id: Secret<String>,
82+
merchant_config_currency: types::storage::enums::Currency,
83+
}
84+
85+
impl TryFrom<&Option<pii::SecretSerdeValue>> for BraintreeMeta {
86+
type Error = error_stack::Report<errors::ConnectorError>;
87+
fn try_from(meta_data: &Option<pii::SecretSerdeValue>) -> Result<Self, Self::Error> {
88+
let metadata: Self = utils::to_connector_meta_from_secret::<Self>(meta_data.clone())
89+
.change_context(errors::ConnectorError::InvalidConfig {
90+
field_name: "merchant connector account metadata",
91+
})?;
92+
Ok(metadata)
93+
}
8294
}
8395

8496
#[derive(Debug, Serialize)]
@@ -96,10 +108,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>>
96108
item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>,
97109
) -> Result<Self, Self::Error> {
98110
let metadata: BraintreeMeta =
99-
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?;
111+
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())
112+
.change_context(errors::ConnectorError::InvalidConfig {
113+
field_name: "merchant connector account metadata",
114+
})?;
100115
utils::validate_currency(
101116
item.router_data.request.currency,
102-
metadata.merchant_config_currency,
117+
Some(metadata.merchant_config_currency),
103118
)?;
104119

105120
match item.router_data.request.payment_method_data.clone() {
@@ -140,26 +155,28 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
140155
fn try_from(
141156
item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
142157
) -> Result<Self, Self::Error> {
143-
match item.router_data.request.payment_method_data.clone() {
144-
Some(api::PaymentMethodData::Card(_)) => {
158+
match item.router_data.payment_method {
159+
api_models::enums::PaymentMethod::Card => {
145160
Ok(Self::Card(CardPaymentRequest::try_from(item)?))
146161
}
147-
Some(api_models::payments::PaymentMethodData::CardRedirect(_))
148-
| Some(api_models::payments::PaymentMethodData::Wallet(_))
149-
| Some(api_models::payments::PaymentMethodData::PayLater(_))
150-
| Some(api_models::payments::PaymentMethodData::BankRedirect(_))
151-
| Some(api_models::payments::PaymentMethodData::BankDebit(_))
152-
| Some(api_models::payments::PaymentMethodData::BankTransfer(_))
153-
| Some(api_models::payments::PaymentMethodData::Crypto(_))
154-
| Some(api_models::payments::PaymentMethodData::MandatePayment)
155-
| Some(api_models::payments::PaymentMethodData::Reward)
156-
| Some(api_models::payments::PaymentMethodData::Upi(_))
157-
| Some(api_models::payments::PaymentMethodData::Voucher(_))
158-
| Some(api_models::payments::PaymentMethodData::GiftCard(_))
159-
| None => Err(errors::ConnectorError::NotImplemented(
160-
utils::get_unimplemented_payment_method_error_message("complete authorize flow"),
161-
)
162-
.into()),
162+
api_models::enums::PaymentMethod::CardRedirect
163+
| api_models::enums::PaymentMethod::PayLater
164+
| api_models::enums::PaymentMethod::Wallet
165+
| api_models::enums::PaymentMethod::BankRedirect
166+
| api_models::enums::PaymentMethod::BankTransfer
167+
| api_models::enums::PaymentMethod::Crypto
168+
| api_models::enums::PaymentMethod::BankDebit
169+
| api_models::enums::PaymentMethod::Reward
170+
| api_models::enums::PaymentMethod::Upi
171+
| api_models::enums::PaymentMethod::Voucher
172+
| api_models::enums::PaymentMethod::GiftCard => {
173+
Err(errors::ConnectorError::NotImplemented(
174+
utils::get_unimplemented_payment_method_error_message(
175+
"complete authorize flow",
176+
),
177+
)
178+
.into())
179+
}
163180
}
164181
}
165182
}
@@ -249,7 +266,6 @@ impl<F>
249266
*client_token_data,
250267
item.data.get_payment_method_token()?,
251268
item.data.request.payment_method_data.clone(),
252-
item.data.request.amount,
253269
)?),
254270
mandate_reference: None,
255271
connector_metadata: None,
@@ -428,7 +444,6 @@ impl<F>
428444
*client_token_data,
429445
item.data.get_payment_method_token()?,
430446
item.data.request.payment_method_data.clone(),
431-
item.data.request.amount,
432447
)?),
433448
mandate_reference: None,
434449
connector_metadata: None,
@@ -586,23 +601,22 @@ impl<F> TryFrom<BraintreeRouterData<&types::RefundsRouterData<F>>> for Braintree
586601
item: BraintreeRouterData<&types::RefundsRouterData<F>>,
587602
) -> Result<Self, Self::Error> {
588603
let metadata: BraintreeMeta =
589-
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?;
604+
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())
605+
.change_context(errors::ConnectorError::InvalidConfig {
606+
field_name: "merchant connector account metadata",
607+
})?;
590608

591609
utils::validate_currency(
592610
item.router_data.request.currency,
593-
metadata.merchant_config_currency,
611+
Some(metadata.merchant_config_currency),
594612
)?;
595613
let query = REFUND_TRANSACTION_MUTATION.to_string();
596614
let variables = BraintreeRefundVariables {
597615
input: BraintreeRefundInput {
598616
transaction_id: item.router_data.request.connector_transaction_id.clone(),
599617
refund: RefundInputData {
600618
amount: item.amount,
601-
merchant_account_id: metadata.merchant_account_id.ok_or(
602-
errors::ConnectorError::MissingRequiredField {
603-
field_name: "merchant_account_id",
604-
},
605-
)?,
619+
merchant_account_id: metadata.merchant_account_id,
606620
},
607621
},
608622
};
@@ -695,9 +709,16 @@ pub struct BraintreeRSyncRequest {
695709
impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest {
696710
type Error = error_stack::Report<errors::ConnectorError>;
697711
fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> {
698-
let metadata: BraintreeMeta =
699-
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
700-
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?;
712+
let metadata: BraintreeMeta = utils::to_connector_meta_from_secret(
713+
item.connector_meta_data.clone(),
714+
)
715+
.change_context(errors::ConnectorError::InvalidConfig {
716+
field_name: "merchant connector account metadata",
717+
})?;
718+
utils::validate_currency(
719+
item.request.currency,
720+
Some(metadata.merchant_config_currency),
721+
)?;
701722
let refund_id = item.request.get_connector_refund_id()?;
702723
let query = format!("query {{ search {{ refunds(input: {{ id: {{is: \"{}\"}} }}, first: 1) {{ edges {{ node {{ id status createdAt amount {{ value currencyCode }} orderId }} }} }} }} }}",refund_id);
703724

@@ -1250,6 +1271,13 @@ pub struct BraintreeThreeDsResponse {
12501271
pub liability_shift_possible: bool,
12511272
}
12521273

1274+
#[derive(Debug, Deserialize)]
1275+
#[serde(rename_all = "camelCase")]
1276+
pub struct BraintreeThreeDsErrorResponse {
1277+
pub code: String,
1278+
pub message: String,
1279+
}
1280+
12531281
#[derive(Debug, Deserialize)]
12541282
pub struct BraintreeRedirectionResponse {
12551283
pub authentication_response: String,
@@ -1263,11 +1291,7 @@ impl TryFrom<BraintreeMeta> for BraintreeClientTokenRequest {
12631291
variables: VariableClientTokenInput {
12641292
input: InputClientTokenData {
12651293
client_token: ClientTokenInput {
1266-
merchant_account_id: metadata.merchant_account_id.ok_or(
1267-
errors::ConnectorError::MissingRequiredField {
1268-
field_name: "merchant_account_id",
1269-
},
1270-
)?,
1294+
merchant_account_id: metadata.merchant_account_id,
12711295
},
12721296
},
12731297
},
@@ -1304,11 +1328,7 @@ impl
13041328
},
13051329
transaction: TransactionBody {
13061330
amount: item.amount.to_owned(),
1307-
merchant_account_id: metadata.merchant_account_id.ok_or(
1308-
errors::ConnectorError::MissingRequiredField {
1309-
field_name: "merchant_account_id",
1310-
},
1311-
)?,
1331+
merchant_account_id: metadata.merchant_account_id,
13121332
},
13131333
},
13141334
},
@@ -1324,10 +1344,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
13241344
item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
13251345
) -> Result<Self, Self::Error> {
13261346
let metadata: BraintreeMeta =
1327-
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?;
1347+
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())
1348+
.change_context(errors::ConnectorError::InvalidConfig {
1349+
field_name: "merchant connector account metadata",
1350+
})?;
13281351
utils::validate_currency(
13291352
item.router_data.request.currency,
1330-
metadata.merchant_config_currency,
1353+
Some(metadata.merchant_config_currency),
13311354
)?;
13321355
let payload_data =
13331356
utils::PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload(
@@ -1360,11 +1383,7 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
13601383
payment_method_id: three_ds_data.nonce,
13611384
transaction: TransactionBody {
13621385
amount: item.amount.to_owned(),
1363-
merchant_account_id: metadata.merchant_account_id.ok_or(
1364-
errors::ConnectorError::MissingRequiredField {
1365-
field_name: "merchant_account_id",
1366-
},
1367-
)?,
1386+
merchant_account_id: metadata.merchant_account_id,
13681387
},
13691388
},
13701389
},
@@ -1376,7 +1395,6 @@ fn get_braintree_redirect_form(
13761395
client_token_data: ClientTokenResponse,
13771396
payment_method_token: types::PaymentMethodToken,
13781397
card_details: api_models::payments::PaymentMethodData,
1379-
amount: i64,
13801398
) -> Result<services::RedirectForm, error_stack::Report<errors::ConnectorError>> {
13811399
Ok(services::RedirectForm::Braintree {
13821400
client_token: client_token_data
@@ -1409,7 +1427,6 @@ fn get_braintree_redirect_form(
14091427
errors::ConnectorError::NotImplemented("given payment method".to_owned()),
14101428
)?,
14111429
},
1412-
amount,
14131430
})
14141431
}
14151432

crates/router/src/core/admin.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use common_utils::{
33
crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue},
44
date_time,
55
ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt},
6+
pii,
67
};
78
use data_models::MerchantStorageScheme;
89
use error_stack::{report, FutureExt, ResultExt};
@@ -656,15 +657,26 @@ pub async fn create_payment_connector(
656657
expected_format: "auth_type and api_key".to_string(),
657658
})?;
658659

659-
validate_auth_type(req.connector_name, &auth).map_err(|err| {
660-
if err.current_context() == &errors::ConnectorError::InvalidConnectorName {
661-
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
662-
message: "The connector name is invalid".to_string(),
663-
})
664-
} else {
665-
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
666-
message: "The auth type is invalid for the connector".to_string(),
667-
})
660+
validate_auth_and_metadata_type(req.connector_name, &auth, &req.metadata).map_err(|err| {
661+
match *err.current_context() {
662+
errors::ConnectorError::InvalidConnectorName => {
663+
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
664+
message: "The connector name is invalid".to_string(),
665+
})
666+
}
667+
errors::ConnectorError::InvalidConfig { field_name } => {
668+
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
669+
message: format!("The {} is invalid", field_name),
670+
})
671+
}
672+
errors::ConnectorError::FailedToObtainAuthType => {
673+
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
674+
message: "The auth type is invalid for the connector".to_string(),
675+
})
676+
}
677+
_ => err.change_context(errors::ApiErrorResponse::InvalidRequestData {
678+
message: "The request body is invalid".to_string(),
679+
}),
668680
}
669681
})?;
670682

@@ -1250,9 +1262,10 @@ pub async fn update_business_profile(
12501262
))
12511263
}
12521264

1253-
pub(crate) fn validate_auth_type(
1265+
pub(crate) fn validate_auth_and_metadata_type(
12541266
connector_name: api_models::enums::Connector,
12551267
val: &types::ConnectorAuthType,
1268+
connector_meta_data: &Option<pii::SecretSerdeValue>,
12561269
) -> Result<(), error_stack::Report<errors::ConnectorError>> {
12571270
use crate::connector::*;
12581271

@@ -1302,6 +1315,9 @@ pub(crate) fn validate_auth_type(
13021315
}
13031316
api_enums::Connector::Braintree => {
13041317
braintree::transformers::BraintreeAuthType::try_from(val)?;
1318+
braintree::braintree_graphql_transformers::BraintreeMeta::try_from(
1319+
connector_meta_data,
1320+
)?;
13051321
Ok(())
13061322
}
13071323
api_enums::Connector::Cashtocode => {

crates/router/src/core/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ pub enum ConnectorError {
178178
message: String,
179179
connector: &'static str,
180180
},
181+
#[error("Invalid Configuration")]
182+
InvalidConfig { field_name: &'static str },
181183
}
182184

183185
#[derive(Debug, thiserror::Error)]

0 commit comments

Comments
 (0)