fix(connector): [braintree] add 3ds redirection error mapping and metadata validation (#2552)

This commit is contained in:
SamraatBansal
2023-10-12 19:05:37 +05:30
committed by GitHub
parent efa53204e8
commit 28d02f94c6
5 changed files with 150 additions and 71 deletions

View File

@ -1496,10 +1496,45 @@ impl services::ConnectorRedirectResponse for Braintree {
fn get_flow_type(
&self,
_query_params: &str,
_json_payload: Option<serde_json::Value>,
_action: services::PaymentAction,
json_payload: Option<serde_json::Value>,
action: services::PaymentAction,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Trigger)
match action {
services::PaymentAction::PSync => match json_payload {
Some(payload) => {
let redirection_response:braintree_graphql_transformers::BraintreeRedirectionResponse = serde_json::from_value(payload)
.into_report()
.change_context(
errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "redirection_response",
},
)?;
let braintree_payload =
serde_json::from_str::<
braintree_graphql_transformers::BraintreeThreeDsErrorResponse,
>(&redirection_response.authentication_response);
let (error_code, error_message) = match braintree_payload {
Ok(braintree_response_payload) => (
braintree_response_payload.code,
braintree_response_payload.message,
),
Err(_) => (
consts::NO_ERROR_CODE.to_string(),
redirection_response.authentication_response,
),
};
Ok(payments::CallConnectorAction::StatusUpdate {
status: enums::AttemptStatus::AuthenticationFailed,
error_code: Some(error_code),
error_message: Some(error_message),
})
}
None => Ok(payments::CallConnectorAction::Avoid),
},
services::PaymentAction::CompleteAuthorize => {
Ok(payments::CallConnectorAction::Trigger)
}
}
}
}

View File

@ -1,3 +1,4 @@
use common_utils::pii;
use error_stack::{IntoReport, ResultExt};
use masking::{ExposeInterface, Secret};
use serde::{Deserialize, Serialize};
@ -77,8 +78,19 @@ pub enum BraintreePaymentsRequest {
#[derive(Debug, Deserialize)]
pub struct BraintreeMeta {
merchant_account_id: Option<Secret<String>>,
merchant_config_currency: Option<types::storage::enums::Currency>,
merchant_account_id: Secret<String>,
merchant_config_currency: types::storage::enums::Currency,
}
impl TryFrom<&Option<pii::SecretSerdeValue>> for BraintreeMeta {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(meta_data: &Option<pii::SecretSerdeValue>) -> Result<Self, Self::Error> {
let metadata: Self = utils::to_connector_meta_from_secret::<Self>(meta_data.clone())
.change_context(errors::ConnectorError::InvalidConfig {
field_name: "merchant connector account metadata",
})?;
Ok(metadata)
}
}
#[derive(Debug, Serialize)]
@ -96,10 +108,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>>
item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?;
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())
.change_context(errors::ConnectorError::InvalidConfig {
field_name: "merchant connector account metadata",
})?;
utils::validate_currency(
item.router_data.request.currency,
metadata.merchant_config_currency,
Some(metadata.merchant_config_currency),
)?;
match item.router_data.request.payment_method_data.clone() {
@ -140,26 +155,28 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
fn try_from(
item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
match item.router_data.request.payment_method_data.clone() {
Some(api::PaymentMethodData::Card(_)) => {
match item.router_data.payment_method {
api_models::enums::PaymentMethod::Card => {
Ok(Self::Card(CardPaymentRequest::try_from(item)?))
}
Some(api_models::payments::PaymentMethodData::CardRedirect(_))
| Some(api_models::payments::PaymentMethodData::Wallet(_))
| Some(api_models::payments::PaymentMethodData::PayLater(_))
| Some(api_models::payments::PaymentMethodData::BankRedirect(_))
| Some(api_models::payments::PaymentMethodData::BankDebit(_))
| Some(api_models::payments::PaymentMethodData::BankTransfer(_))
| Some(api_models::payments::PaymentMethodData::Crypto(_))
| Some(api_models::payments::PaymentMethodData::MandatePayment)
| Some(api_models::payments::PaymentMethodData::Reward)
| Some(api_models::payments::PaymentMethodData::Upi(_))
| Some(api_models::payments::PaymentMethodData::Voucher(_))
| Some(api_models::payments::PaymentMethodData::GiftCard(_))
| None => Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("complete authorize flow"),
)
.into()),
api_models::enums::PaymentMethod::CardRedirect
| api_models::enums::PaymentMethod::PayLater
| api_models::enums::PaymentMethod::Wallet
| api_models::enums::PaymentMethod::BankRedirect
| api_models::enums::PaymentMethod::BankTransfer
| api_models::enums::PaymentMethod::Crypto
| api_models::enums::PaymentMethod::BankDebit
| api_models::enums::PaymentMethod::Reward
| api_models::enums::PaymentMethod::Upi
| api_models::enums::PaymentMethod::Voucher
| api_models::enums::PaymentMethod::GiftCard => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message(
"complete authorize flow",
),
)
.into())
}
}
}
}
@ -249,7 +266,6 @@ impl<F>
*client_token_data,
item.data.get_payment_method_token()?,
item.data.request.payment_method_data.clone(),
item.data.request.amount,
)?),
mandate_reference: None,
connector_metadata: None,
@ -428,7 +444,6 @@ impl<F>
*client_token_data,
item.data.get_payment_method_token()?,
item.data.request.payment_method_data.clone(),
item.data.request.amount,
)?),
mandate_reference: None,
connector_metadata: None,
@ -586,11 +601,14 @@ impl<F> TryFrom<BraintreeRouterData<&types::RefundsRouterData<F>>> for Braintree
item: BraintreeRouterData<&types::RefundsRouterData<F>>,
) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?;
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())
.change_context(errors::ConnectorError::InvalidConfig {
field_name: "merchant connector account metadata",
})?;
utils::validate_currency(
item.router_data.request.currency,
metadata.merchant_config_currency,
Some(metadata.merchant_config_currency),
)?;
let query = REFUND_TRANSACTION_MUTATION.to_string();
let variables = BraintreeRefundVariables {
@ -598,11 +616,7 @@ impl<F> TryFrom<BraintreeRouterData<&types::RefundsRouterData<F>>> for Braintree
transaction_id: item.router_data.request.connector_transaction_id.clone(),
refund: RefundInputData {
amount: item.amount,
merchant_account_id: metadata.merchant_account_id.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
merchant_account_id: metadata.merchant_account_id,
},
},
};
@ -695,9 +709,16 @@ pub struct BraintreeRSyncRequest {
impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?;
let metadata: BraintreeMeta = utils::to_connector_meta_from_secret(
item.connector_meta_data.clone(),
)
.change_context(errors::ConnectorError::InvalidConfig {
field_name: "merchant connector account metadata",
})?;
utils::validate_currency(
item.request.currency,
Some(metadata.merchant_config_currency),
)?;
let refund_id = item.request.get_connector_refund_id()?;
let query = format!("query {{ search {{ refunds(input: {{ id: {{is: \"{}\"}} }}, first: 1) {{ edges {{ node {{ id status createdAt amount {{ value currencyCode }} orderId }} }} }} }} }}",refund_id);
@ -1250,6 +1271,13 @@ pub struct BraintreeThreeDsResponse {
pub liability_shift_possible: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BraintreeThreeDsErrorResponse {
pub code: String,
pub message: String,
}
#[derive(Debug, Deserialize)]
pub struct BraintreeRedirectionResponse {
pub authentication_response: String,
@ -1263,11 +1291,7 @@ impl TryFrom<BraintreeMeta> for BraintreeClientTokenRequest {
variables: VariableClientTokenInput {
input: InputClientTokenData {
client_token: ClientTokenInput {
merchant_account_id: metadata.merchant_account_id.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
merchant_account_id: metadata.merchant_account_id,
},
},
},
@ -1304,11 +1328,7 @@ impl
},
transaction: TransactionBody {
amount: item.amount.to_owned(),
merchant_account_id: metadata.merchant_account_id.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
merchant_account_id: metadata.merchant_account_id,
},
},
},
@ -1324,10 +1344,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?;
utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())
.change_context(errors::ConnectorError::InvalidConfig {
field_name: "merchant connector account metadata",
})?;
utils::validate_currency(
item.router_data.request.currency,
metadata.merchant_config_currency,
Some(metadata.merchant_config_currency),
)?;
let payload_data =
utils::PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload(
@ -1360,11 +1383,7 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
payment_method_id: three_ds_data.nonce,
transaction: TransactionBody {
amount: item.amount.to_owned(),
merchant_account_id: metadata.merchant_account_id.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
merchant_account_id: metadata.merchant_account_id,
},
},
},
@ -1376,7 +1395,6 @@ fn get_braintree_redirect_form(
client_token_data: ClientTokenResponse,
payment_method_token: types::PaymentMethodToken,
card_details: api_models::payments::PaymentMethodData,
amount: i64,
) -> Result<services::RedirectForm, error_stack::Report<errors::ConnectorError>> {
Ok(services::RedirectForm::Braintree {
client_token: client_token_data
@ -1409,7 +1427,6 @@ fn get_braintree_redirect_form(
errors::ConnectorError::NotImplemented("given payment method".to_owned()),
)?,
},
amount,
})
}

View File

@ -3,6 +3,7 @@ use common_utils::{
crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue},
date_time,
ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt},
pii,
};
use data_models::MerchantStorageScheme;
use error_stack::{report, FutureExt, ResultExt};
@ -656,15 +657,26 @@ pub async fn create_payment_connector(
expected_format: "auth_type and api_key".to_string(),
})?;
validate_auth_type(req.connector_name, &auth).map_err(|err| {
if err.current_context() == &errors::ConnectorError::InvalidConnectorName {
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "The connector name is invalid".to_string(),
})
} else {
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "The auth type is invalid for the connector".to_string(),
})
validate_auth_and_metadata_type(req.connector_name, &auth, &req.metadata).map_err(|err| {
match *err.current_context() {
errors::ConnectorError::InvalidConnectorName => {
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "The connector name is invalid".to_string(),
})
}
errors::ConnectorError::InvalidConfig { field_name } => {
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: format!("The {} is invalid", field_name),
})
}
errors::ConnectorError::FailedToObtainAuthType => {
err.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "The auth type is invalid for the connector".to_string(),
})
}
_ => err.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "The request body is invalid".to_string(),
}),
}
})?;
@ -1250,9 +1262,10 @@ pub async fn update_business_profile(
))
}
pub(crate) fn validate_auth_type(
pub(crate) fn validate_auth_and_metadata_type(
connector_name: api_models::enums::Connector,
val: &types::ConnectorAuthType,
connector_meta_data: &Option<pii::SecretSerdeValue>,
) -> Result<(), error_stack::Report<errors::ConnectorError>> {
use crate::connector::*;
@ -1302,6 +1315,9 @@ pub(crate) fn validate_auth_type(
}
api_enums::Connector::Braintree => {
braintree::transformers::BraintreeAuthType::try_from(val)?;
braintree::braintree_graphql_transformers::BraintreeMeta::try_from(
connector_meta_data,
)?;
Ok(())
}
api_enums::Connector::Cashtocode => {

View File

@ -178,6 +178,8 @@ pub enum ConnectorError {
message: String,
connector: &'static str,
},
#[error("Invalid Configuration")]
InvalidConfig { field_name: &'static str },
}
#[derive(Debug, thiserror::Error)]

View File

@ -703,7 +703,6 @@ pub enum RedirectForm {
client_token: String,
card_token: String,
bin: String,
amount: i64,
},
}
@ -1262,7 +1261,6 @@ pub fn build_redirection_form(
client_token,
card_token,
bin,
amount,
} => {
maud::html! {
(maud::DOCTYPE)
@ -1270,7 +1268,7 @@ pub fn build_redirection_form(
head {
meta name="viewport" content="width=device-width, initial-scale=1";
(PreEscaped(r#"<script src="https://js.braintreegateway.com/web/3.97.1/js/three-d-secure.js"></script>"#))
(PreEscaped(r#"<script src="https://js.braintreegateway.com/web/3.97.1/js/hosted-fields.js"></script>"#))
// (PreEscaped(r#"<script src="https://js.braintreegateway.com/web/3.97.1/js/hosted-fields.js"></script>"#))
}
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
@ -1318,15 +1316,26 @@ pub fn build_redirection_form(
}}
}},
onLookupComplete: function(data, next) {{
console.log(\"onLookup Complete\", data);
// console.log(\"onLookup Complete\", data);
next();
}}
}},
function(err, payload) {{
if(err) {{
console.error(err);
var f = document.createElement('form');
f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/response/braintree\");
var i = document.createElement('input');
i.type = 'hidden';
f.method='POST';
i.name = 'authentication_response';
i.value = JSON.stringify(err);
f.appendChild(i);
f.body = JSON.stringify(err);
document.body.appendChild(f);
f.submit();
}} else {{
console.log(payload);
// console.log(payload);
var f = document.createElement('form');
f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/braintree\");
var i = document.createElement('input');