Skip to content

Commit

Permalink
refactor(connector): [Bluesnap] Enahnce 3ds Flow (#2115)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamraatBansal authored Sep 16, 2023
1 parent 1d5eece commit 272f5e4
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 167 deletions.
2 changes: 1 addition & 1 deletion config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
153 changes: 41 additions & 112 deletions crates/router/src/connector/bluesnap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -650,32 +565,45 @@ 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"
)),
))
}
}

fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let connector_req = bluesnap::BluesnapPaymentsRequest::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))
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,
utils::Encode::<bluesnap::BluesnapPaymentsRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(bluesnap_req))
}
}
}

fn build_request(
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
100 changes: 68 additions & 32 deletions crates/router/src/connector/bluesnap/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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,
})
}
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 272f5e4

Please sign in to comment.