mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
refactor(connector): [Bluesnap] Enahnce 3ds Flow (#2115)
This commit is contained in:
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -212,6 +212,7 @@ default_imp_for_create_customer!(
|
||||
connector::Authorizedotnet,
|
||||
connector::Bambora,
|
||||
connector::Bitpay,
|
||||
connector::Bluesnap,
|
||||
connector::Boku,
|
||||
connector::Braintree,
|
||||
connector::Cashtocode,
|
||||
|
||||
@ -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>
|
||||
")))
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user