refactor(connector): [Bluesnap] Enahnce 3ds Flow (#2115)

This commit is contained in:
SamraatBansal
2023-09-16 23:46:11 +05:30
committed by GitHub
parent 1d5eecef04
commit 272f5e4c1f
8 changed files with 142 additions and 167 deletions

View File

@ -398,7 +398,7 @@ ach = { currency = "USD" }
cashapp = {country = "US", currency = "USD"}
[connector_customer]
connector_list = "stax"
connector_list = "stax,stripe"
payout_connector_list = "wise"
[bank_config.online_banking_fpx]

View File

@ -378,7 +378,7 @@ trustpay = {payment_method = "card,bank_redirect,wallet"}
stripe = {payment_method = "card,bank_redirect,pay_later,wallet,bank_debit"}
[connector_customer]
connector_list = "bluesnap,stax,stripe"
connector_list = "stax,stripe"
payout_connector_list = "wise"
[dummy_connector]

View File

@ -292,7 +292,7 @@ card.credit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay
card.debit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"}
[connector_customer]
connector_list = "stax"
connector_list = "stax,stripe"
payout_connector_list = "wise"
[multiple_api_version_supported_connectors]

View File

@ -157,6 +157,15 @@ impl ConnectorValidation for Bluesnap {
&self,
data: &types::PaymentsSyncRouterData,
) -> CustomResult<(), errors::ConnectorError> {
// If 3DS payment was triggered, connector will have context about payment in CompleteAuthorizeFlow and thus can't make force_sync
if data.is_three_ds() && data.status == enums::AttemptStatus::AuthenticationPending {
return Err(
errors::ConnectorError::MissingConnectorRelatedTransactionID {
id: "connector_transaction_id".to_string(),
},
)
.into_report();
}
// if connector_transaction_id is present, psync can be made
if data
.request
@ -194,100 +203,6 @@ impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::Payments
{
}
impl api::ConnectorCustomer for Bluesnap {}
impl
ConnectorIntegration<
api::CreateConnectorCustomer,
types::ConnectorCustomerData,
types::PaymentsResponseData,
> for Bluesnap
{
fn get_headers(
&self,
req: &types::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}services/2/vaulted-shoppers",
self.base_url(connectors),
))
}
fn get_request_body(
&self,
req: &types::ConnectorCustomerRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let connector_request = bluesnap::BluesnapCustomerRequest::try_from(req)?;
let bluesnap_req = types::RequestBody::log_and_get_request_body(
&connector_request,
utils::Encode::<bluesnap::BluesnapCustomerRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bluesnap_req))
}
fn build_request(
&self,
req: &types::ConnectorCustomerRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::ConnectorCustomerType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::ConnectorCustomerType::get_headers(
self, req, connectors,
)?)
.body(types::ConnectorCustomerType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::ConnectorCustomerRouterData,
res: Response,
) -> CustomResult<types::ConnectorCustomerRouterData, errors::ConnectorError>
where
types::PaymentsResponseData: Clone,
{
let response: bluesnap::BluesnapCustomerResponse = res
.response
.parse_struct("BluesnapCustomerResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentVoid for Bluesnap {}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
@ -650,18 +565,18 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
match req.is_three_ds() && !req.request.is_wallet() {
true => Ok(format!(
"{}{}{}",
if req.is_three_ds() && req.request.is_card() {
Ok(format!(
"{}{}",
self.base_url(connectors),
"services/2/payment-fields-tokens?shopperId=",
req.get_connector_customer_id()?
)),
_ => Ok(format!(
"services/2/payment-fields-tokens/prefill",
))
} else {
Ok(format!(
"{}{}",
self.base_url(connectors),
"services/2/transactions"
)),
))
}
}
@ -669,6 +584,17 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
match req.is_three_ds() && req.request.is_card() {
true => {
let connector_req = bluesnap::BluesnapPaymentsTokenRequest::try_from(req)?;
let bluesnap_req = types::RequestBody::log_and_get_request_body(
&connector_req,
utils::Encode::<bluesnap::BluesnapPaymentsRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bluesnap_req))
}
_ => {
let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(req)?;
let bluesnap_req = types::RequestBody::log_and_get_request_body(
&connector_req,
@ -677,6 +603,8 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bluesnap_req))
}
}
}
fn build_request(
&self,
@ -704,9 +632,10 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: &types::PaymentsAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
match (data.is_three_ds() && !data.request.is_wallet(), res.headers) {
match (data.is_three_ds() && data.request.is_card(), res.headers) {
(true, Some(headers)) => {
let location = connector_utils::get_http_header("Location", &headers)?;
let location = connector_utils::get_http_header("Location", &headers)
.change_context(errors::ConnectorError::ResponseHandlingFailed)?; // If location headers are not present connector will return 4XX so this error will never be propagated
let payment_fields_token = location
.split('/')
.last()
@ -783,7 +712,7 @@ impl
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(req)?;
let connector_req = bluesnap::BluesnapCompletePaymentsRequest::try_from(req)?;
let bluesnap_req = types::RequestBody::log_and_get_request_body(
&connector_req,
utils::Encode::<bluesnap::BluesnapPaymentsRequest>::encode_to_string_of_json,

View File

@ -6,7 +6,7 @@ use common_utils::{
pii::Email,
};
use error_stack::{IntoReport, ResultExt};
use masking::ExposeInterface;
use masking::{ExposeInterface, PeekInterface};
use serde::{Deserialize, Serialize};
use crate::{
@ -161,6 +161,42 @@ pub struct BluesnapConnectorMetaData {
pub merchant_id: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapPaymentsTokenRequest {
cc_number: cards::CardNumber,
exp_date: Secret<String>,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsTokenRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data {
api::PaymentMethodData::Card(ref ccard) => Ok(Self {
cc_number: ccard.card_number.clone(),
exp_date: ccard.get_expiry_date_as_mmyyyy("/"),
}),
api::PaymentMethodData::Wallet(_)
| payments::PaymentMethodData::PayLater(_)
| payments::PaymentMethodData::BankRedirect(_)
| payments::PaymentMethodData::BankDebit(_)
| payments::PaymentMethodData::BankTransfer(_)
| payments::PaymentMethodData::Crypto(_)
| payments::PaymentMethodData::MandatePayment
| payments::PaymentMethodData::Reward
| payments::PaymentMethodData::Upi(_)
| payments::PaymentMethodData::CardRedirect(_)
| payments::PaymentMethodData::Voucher(_)
| payments::PaymentMethodData::GiftCard(_) => {
Err(errors::ConnectorError::NotImplemented(
"Selected payment method via Token flow through bluesnap".to_string(),
))
.into_report()
}
}
}
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
@ -444,7 +480,20 @@ impl TryFrom<types::PaymentsSessionResponseRouterData<BluesnapWalletTokenRespons
}
}
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRequest {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapCompletePaymentsRequest {
amount: String,
currency: enums::Currency,
card_transaction_type: BluesnapTxnType,
pf_token: String,
three_d_secure: Option<BluesnapThreeDSecureInfo>,
transaction_fraud_info: Option<TransactionFraudInfo>,
card_holder_info: Option<BluesnapCardHolderInfo>,
merchant_transaction_id: Option<String>,
}
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapCompletePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> {
let redirection_response: BluesnapRedirectionResponse = item
@ -458,6 +507,22 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRe
.parse_value("BluesnapRedirectionResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let pf_token = item
.request
.redirect_response
.clone()
.and_then(|res| res.params.to_owned())
.ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "request.redirect_response.params",
})?
.peek()
.split_once('=')
.ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "request.redirect_response.params.paymentToken",
})?
.1
.to_string();
let redirection_result: BluesnapThreeDsResult = redirection_response
.authentication_response
.parse_struct("BluesnapThreeDsResult")
@ -467,23 +532,8 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRe
Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly,
_ => BluesnapTxnType::AuthCapture,
};
let payment_method = if let Some(api::PaymentMethodData::Card(ccard)) =
item.request.payment_method_data.clone()
{
PaymentMethodDetails::CreditCard(Card {
card_number: ccard.card_number.clone(),
expiration_month: ccard.card_exp_month.clone(),
expiration_year: ccard.get_expiry_year_4_digit(),
security_code: ccard.card_cvc,
})
} else {
Err(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "request.payment_method_data",
})?
};
Ok(Self {
amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
payment_method,
currency: item.request.currency,
card_transaction_type: auth_mode,
three_d_secure: Some(BluesnapThreeDSecureInfo {
@ -502,6 +552,7 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRe
item.request.get_email()?,
)?,
merchant_transaction_id: Some(item.connector_request_reference_id.clone()),
pf_token,
})
}
}
@ -594,21 +645,6 @@ impl TryFrom<&types::ConnectorAuthType> for BluesnapAuthType {
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapCustomerRequest {
email: Option<Email>,
}
impl TryFrom<&types::ConnectorCustomerRouterData> for BluesnapCustomerRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::ConnectorCustomerRouterData) -> Result<Self, Self::Error> {
Ok(Self {
email: item.request.email.to_owned(),
})
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapCustomerResponse {

View File

@ -262,6 +262,7 @@ pub trait PaymentsAuthorizeRequestData {
fn get_webhook_url(&self) -> Result<String, Error>;
fn get_router_return_url(&self) -> Result<String, Error>;
fn is_wallet(&self) -> bool;
fn is_card(&self) -> bool;
fn get_payment_method_type(&self) -> Result<diesel_models::enums::PaymentMethodType, Error>;
fn get_connector_mandate_id(&self) -> Result<String, Error>;
fn get_complete_authorize_url(&self) -> Result<String, Error>;
@ -338,6 +339,9 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
fn is_wallet(&self) -> bool {
matches!(self.payment_method_data, api::PaymentMethodData::Wallet(_))
}
fn is_card(&self) -> bool {
matches!(self.payment_method_data, api::PaymentMethodData::Card(_))
}
fn get_payment_method_type(&self) -> Result<diesel_models::enums::PaymentMethodType, Error> {
self.payment_method_type
@ -591,6 +595,7 @@ pub trait CardData {
delimiter: String,
) -> Secret<String>;
fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret<String>;
fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret<String>;
fn get_expiry_year_4_digit(&self) -> Secret<String>;
fn get_expiry_date_as_yymm(&self) -> Secret<String>;
}
@ -625,6 +630,15 @@ impl CardData for api::Card {
self.card_exp_month.peek().clone()
))
}
fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret<String> {
let year = self.get_expiry_year_4_digit();
Secret::new(format!(
"{}{}{}",
self.card_exp_month.peek().clone(),
delimiter,
year.peek()
))
}
fn get_expiry_year_4_digit(&self) -> Secret<String> {
let mut year = self.card_exp_year.peek().clone();
if year.len() == 2 {

View File

@ -212,6 +212,7 @@ default_imp_for_create_customer!(
connector::Authorizedotnet,
connector::Bambora,
connector::Bitpay,
connector::Bluesnap,
connector::Boku,
connector::Braintree,
connector::Cashtocode,

View File

@ -1058,29 +1058,24 @@ pub fn build_redirection_form(
RedirectForm::BlueSnap {
payment_fields_token,
} => {
let card_details = if let Some(api::PaymentMethodData::Card(ccard)) =
payment_method_data
{
let card_details =
if let Some(api::PaymentMethodData::Card(ccard)) = payment_method_data {
format!(
"var newCard={{ccNumber: \"{}\",cvv: \"{}\",expDate: \"{}/{}\",amount: {},currency: \"{}\"}};",
ccard.card_number.peek(),
"var saveCardDirectly={{cvv: \"{}\",amount: {},currency: \"{}\"}};",
ccard.card_cvc.peek(),
ccard.card_exp_month.peek(),
ccard.card_exp_year.peek(),
amount,
currency
)
} else {
"".to_string()
};
let bluesnap_url = config.connectors.bluesnap.secondary_base_url;
let bluesnap_sdk_url = config.connectors.bluesnap.secondary_base_url;
maud::html! {
(maud::DOCTYPE)
html {
head {
meta name="viewport" content="width=device-width, initial-scale=1";
(PreEscaped(format!("<script src=\"{bluesnap_url}web-sdk/5/bluesnap.js\"></script>")))
(PreEscaped(format!("<script src=\"{bluesnap_sdk_url}web-sdk/5/bluesnap.js\"></script>")))
}
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
@ -1110,7 +1105,7 @@ pub fn build_redirection_form(
function(sdkResponse) {{
console.log(sdkResponse);
var f = document.createElement('form');
f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/bluesnap\");
f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/bluesnap?paymentToken={payment_fields_token}\");
f.method='POST';
var i=document.createElement('input');
i.type='hidden';
@ -1121,7 +1116,7 @@ pub fn build_redirection_form(
f.submit();
}});
{card_details}
bluesnap.threeDsPaymentsSubmitData(newCard);
bluesnap.threeDsPaymentsSubmitData(saveCardDirectly);
</script>
")))
}}