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( fn get_flow_type(
&self, &self,
_query_params: &str, _query_params: &str,
_json_payload: Option<serde_json::Value>, json_payload: Option<serde_json::Value>,
_action: services::PaymentAction, action: services::PaymentAction,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> { ) -> 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 error_stack::{IntoReport, ResultExt};
use masking::{ExposeInterface, Secret}; use masking::{ExposeInterface, Secret};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -77,8 +78,19 @@ pub enum BraintreePaymentsRequest {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BraintreeMeta { pub struct BraintreeMeta {
merchant_account_id: Option<Secret<String>>, merchant_account_id: Secret<String>,
merchant_config_currency: Option<types::storage::enums::Currency>, 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)] #[derive(Debug, Serialize)]
@ -96,10 +108,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>>
item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta = 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( utils::validate_currency(
item.router_data.request.currency, item.router_data.request.currency,
metadata.merchant_config_currency, Some(metadata.merchant_config_currency),
)?; )?;
match item.router_data.request.payment_method_data.clone() { match item.router_data.request.payment_method_data.clone() {
@ -140,26 +155,28 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
fn try_from( fn try_from(
item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
match item.router_data.request.payment_method_data.clone() { match item.router_data.payment_method {
Some(api::PaymentMethodData::Card(_)) => { api_models::enums::PaymentMethod::Card => {
Ok(Self::Card(CardPaymentRequest::try_from(item)?)) Ok(Self::Card(CardPaymentRequest::try_from(item)?))
} }
Some(api_models::payments::PaymentMethodData::CardRedirect(_)) api_models::enums::PaymentMethod::CardRedirect
| Some(api_models::payments::PaymentMethodData::Wallet(_)) | api_models::enums::PaymentMethod::PayLater
| Some(api_models::payments::PaymentMethodData::PayLater(_)) | api_models::enums::PaymentMethod::Wallet
| Some(api_models::payments::PaymentMethodData::BankRedirect(_)) | api_models::enums::PaymentMethod::BankRedirect
| Some(api_models::payments::PaymentMethodData::BankDebit(_)) | api_models::enums::PaymentMethod::BankTransfer
| Some(api_models::payments::PaymentMethodData::BankTransfer(_)) | api_models::enums::PaymentMethod::Crypto
| Some(api_models::payments::PaymentMethodData::Crypto(_)) | api_models::enums::PaymentMethod::BankDebit
| Some(api_models::payments::PaymentMethodData::MandatePayment) | api_models::enums::PaymentMethod::Reward
| Some(api_models::payments::PaymentMethodData::Reward) | api_models::enums::PaymentMethod::Upi
| Some(api_models::payments::PaymentMethodData::Upi(_)) | api_models::enums::PaymentMethod::Voucher
| Some(api_models::payments::PaymentMethodData::Voucher(_)) | api_models::enums::PaymentMethod::GiftCard => {
| Some(api_models::payments::PaymentMethodData::GiftCard(_)) Err(errors::ConnectorError::NotImplemented(
| None => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message(
utils::get_unimplemented_payment_method_error_message("complete authorize flow"), "complete authorize flow",
) ),
.into()), )
.into())
}
} }
} }
} }
@ -249,7 +266,6 @@ impl<F>
*client_token_data, *client_token_data,
item.data.get_payment_method_token()?, item.data.get_payment_method_token()?,
item.data.request.payment_method_data.clone(), item.data.request.payment_method_data.clone(),
item.data.request.amount,
)?), )?),
mandate_reference: None, mandate_reference: None,
connector_metadata: None, connector_metadata: None,
@ -428,7 +444,6 @@ impl<F>
*client_token_data, *client_token_data,
item.data.get_payment_method_token()?, item.data.get_payment_method_token()?,
item.data.request.payment_method_data.clone(), item.data.request.payment_method_data.clone(),
item.data.request.amount,
)?), )?),
mandate_reference: None, mandate_reference: None,
connector_metadata: None, connector_metadata: None,
@ -586,11 +601,14 @@ impl<F> TryFrom<BraintreeRouterData<&types::RefundsRouterData<F>>> for Braintree
item: BraintreeRouterData<&types::RefundsRouterData<F>>, item: BraintreeRouterData<&types::RefundsRouterData<F>>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta = 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( utils::validate_currency(
item.router_data.request.currency, item.router_data.request.currency,
metadata.merchant_config_currency, Some(metadata.merchant_config_currency),
)?; )?;
let query = REFUND_TRANSACTION_MUTATION.to_string(); let query = REFUND_TRANSACTION_MUTATION.to_string();
let variables = BraintreeRefundVariables { 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(), transaction_id: item.router_data.request.connector_transaction_id.clone(),
refund: RefundInputData { refund: RefundInputData {
amount: item.amount, amount: item.amount,
merchant_account_id: metadata.merchant_account_id.ok_or( merchant_account_id: metadata.merchant_account_id,
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
}, },
}, },
}; };
@ -695,9 +709,16 @@ pub struct BraintreeRSyncRequest {
impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest { impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> { fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta = let metadata: BraintreeMeta = utils::to_connector_meta_from_secret(
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?; item.connector_meta_data.clone(),
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?; )
.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 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); 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, pub liability_shift_possible: bool,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BraintreeThreeDsErrorResponse {
pub code: String,
pub message: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BraintreeRedirectionResponse { pub struct BraintreeRedirectionResponse {
pub authentication_response: String, pub authentication_response: String,
@ -1263,11 +1291,7 @@ impl TryFrom<BraintreeMeta> for BraintreeClientTokenRequest {
variables: VariableClientTokenInput { variables: VariableClientTokenInput {
input: InputClientTokenData { input: InputClientTokenData {
client_token: ClientTokenInput { client_token: ClientTokenInput {
merchant_account_id: metadata.merchant_account_id.ok_or( merchant_account_id: metadata.merchant_account_id,
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
}, },
}, },
}, },
@ -1304,11 +1328,7 @@ impl
}, },
transaction: TransactionBody { transaction: TransactionBody {
amount: item.amount.to_owned(), amount: item.amount.to_owned(),
merchant_account_id: metadata.merchant_account_id.ok_or( merchant_account_id: metadata.merchant_account_id,
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
}, },
}, },
}, },
@ -1324,10 +1344,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta = 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( utils::validate_currency(
item.router_data.request.currency, item.router_data.request.currency,
metadata.merchant_config_currency, Some(metadata.merchant_config_currency),
)?; )?;
let payload_data = let payload_data =
utils::PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload( utils::PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload(
@ -1360,11 +1383,7 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
payment_method_id: three_ds_data.nonce, payment_method_id: three_ds_data.nonce,
transaction: TransactionBody { transaction: TransactionBody {
amount: item.amount.to_owned(), amount: item.amount.to_owned(),
merchant_account_id: metadata.merchant_account_id.ok_or( merchant_account_id: metadata.merchant_account_id,
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
}, },
}, },
}, },
@ -1376,7 +1395,6 @@ fn get_braintree_redirect_form(
client_token_data: ClientTokenResponse, client_token_data: ClientTokenResponse,
payment_method_token: types::PaymentMethodToken, payment_method_token: types::PaymentMethodToken,
card_details: api_models::payments::PaymentMethodData, card_details: api_models::payments::PaymentMethodData,
amount: i64,
) -> Result<services::RedirectForm, error_stack::Report<errors::ConnectorError>> { ) -> Result<services::RedirectForm, error_stack::Report<errors::ConnectorError>> {
Ok(services::RedirectForm::Braintree { Ok(services::RedirectForm::Braintree {
client_token: client_token_data client_token: client_token_data
@ -1409,7 +1427,6 @@ fn get_braintree_redirect_form(
errors::ConnectorError::NotImplemented("given payment method".to_owned()), 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}, crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue},
date_time, date_time,
ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt},
pii,
}; };
use data_models::MerchantStorageScheme; use data_models::MerchantStorageScheme;
use error_stack::{report, FutureExt, ResultExt}; 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(), expected_format: "auth_type and api_key".to_string(),
})?; })?;
validate_auth_type(req.connector_name, &auth).map_err(|err| { validate_auth_and_metadata_type(req.connector_name, &auth, &req.metadata).map_err(|err| {
if err.current_context() == &errors::ConnectorError::InvalidConnectorName { match *err.current_context() {
err.change_context(errors::ApiErrorResponse::InvalidRequestData { errors::ConnectorError::InvalidConnectorName => {
message: "The connector name is invalid".to_string(), 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(), 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, connector_name: api_models::enums::Connector,
val: &types::ConnectorAuthType, val: &types::ConnectorAuthType,
connector_meta_data: &Option<pii::SecretSerdeValue>,
) -> Result<(), error_stack::Report<errors::ConnectorError>> { ) -> Result<(), error_stack::Report<errors::ConnectorError>> {
use crate::connector::*; use crate::connector::*;
@ -1302,6 +1315,9 @@ pub(crate) fn validate_auth_type(
} }
api_enums::Connector::Braintree => { api_enums::Connector::Braintree => {
braintree::transformers::BraintreeAuthType::try_from(val)?; braintree::transformers::BraintreeAuthType::try_from(val)?;
braintree::braintree_graphql_transformers::BraintreeMeta::try_from(
connector_meta_data,
)?;
Ok(()) Ok(())
} }
api_enums::Connector::Cashtocode => { api_enums::Connector::Cashtocode => {

View File

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

View File

@ -703,7 +703,6 @@ pub enum RedirectForm {
client_token: String, client_token: String,
card_token: String, card_token: String,
bin: String, bin: String,
amount: i64,
}, },
} }
@ -1262,7 +1261,6 @@ pub fn build_redirection_form(
client_token, client_token,
card_token, card_token,
bin, bin,
amount,
} => { } => {
maud::html! { maud::html! {
(maud::DOCTYPE) (maud::DOCTYPE)
@ -1270,7 +1268,7 @@ pub fn build_redirection_form(
head { head {
meta name="viewport" content="width=device-width, initial-scale=1"; 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/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;" { 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) {{ onLookupComplete: function(data, next) {{
console.log(\"onLookup Complete\", data); // console.log(\"onLookup Complete\", data);
next(); next();
}} }}
}}, }},
function(err, payload) {{ function(err, payload) {{
if(err) {{ if(err) {{
console.error(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 {{ }} else {{
console.log(payload); // console.log(payload);
var f = document.createElement('form'); var f = document.createElement('form');
f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/braintree\"); f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/braintree\");
var i = document.createElement('input'); var i = document.createElement('input');