From 272f5e4c1f34710fe13b1ede1b938d2f0b76e251 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:46:11 +0530 Subject: [PATCH] refactor(connector): [Bluesnap] Enahnce 3ds Flow (#2115) --- config/config.example.toml | 2 +- config/development.toml | 2 +- config/docker_compose.toml | 2 +- crates/router/src/connector/bluesnap.rs | 153 +++++------------- .../src/connector/bluesnap/transformers.rs | 100 ++++++++---- crates/router/src/connector/utils.rs | 14 ++ crates/router/src/core/payments/flows.rs | 1 + crates/router/src/services/api.rs | 35 ++-- 8 files changed, 142 insertions(+), 167 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 604be8f09149..1eea6ad76abb 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -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] diff --git a/config/development.toml b/config/development.toml index 1d6ed4398045..5652c829f247 100644 --- a/config/development.toml +++ b/config/development.toml @@ -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] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 625f6a6f1ef0..10127ff39f1f 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -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] diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 0da02466ff45..feb45ac6927e 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -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 for Bluesnap -{ - fn get_headers( - &self, - req: &types::ConnectorCustomerRouterData, - connectors: &settings::Connectors, - ) -> CustomResult)>, 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 { - Ok(format!( - "{}services/2/vaulted-shoppers", - self.base_url(connectors), - )) - } - - fn get_request_body( - &self, - req: &types::ConnectorCustomerRouterData, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = bluesnap::BluesnapCustomerRequest::try_from(req)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::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, 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 - 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 { - self.build_error_response(res) - } -} - impl api::PaymentVoid for Bluesnap {} impl ConnectorIntegration @@ -650,18 +565,18 @@ impl ConnectorIntegration CustomResult { - 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,13 +584,26 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(req)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::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::::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::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bluesnap_req)) + } + } } fn build_request( @@ -704,9 +632,10 @@ impl ConnectorIntegration CustomResult { - 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, 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::::encode_to_string_of_json, diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index d2be16530ade..80dc3668ffcf 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -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, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsTokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + 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; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { @@ -444,7 +480,20 @@ impl TryFrom 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, + transaction_fraud_info: Option, + card_holder_info: Option, + merchant_transaction_id: Option, +} + +impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapCompletePaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result { 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, -} - -impl TryFrom<&types::ConnectorCustomerRouterData> for BluesnapCustomerRequest { - type Error = error_stack::Report; - fn try_from(item: &types::ConnectorCustomerRouterData) -> Result { - Ok(Self { - email: item.request.email.to_owned(), - }) - } -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BluesnapCustomerResponse { diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 04c9af069c73..4d9b5507833c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -262,6 +262,7 @@ pub trait PaymentsAuthorizeRequestData { fn get_webhook_url(&self) -> Result; fn get_router_return_url(&self) -> Result; fn is_wallet(&self) -> bool; + fn is_card(&self) -> bool; fn get_payment_method_type(&self) -> Result; fn get_connector_mandate_id(&self) -> Result; fn get_complete_authorize_url(&self) -> Result; @@ -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 { self.payment_method_type @@ -591,6 +595,7 @@ pub trait CardData { delimiter: String, ) -> Secret; fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret; + fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret; fn get_expiry_year_4_digit(&self) -> Secret; fn get_expiry_date_as_yymm(&self) -> Secret; } @@ -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 { + 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 { let mut year = self.card_exp_year.peek().clone(); if year.len() == 2 { diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index eda3739edfcd..b7aea76d5273 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -212,6 +212,7 @@ default_imp_for_create_customer!( connector::Authorizedotnet, connector::Bambora, connector::Bitpay, + connector::Bluesnap, connector::Boku, connector::Braintree, connector::Cashtocode, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 99400050df3c..2678c7d079d8 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -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 - { - format!( - "var newCard={{ccNumber: \"{}\",cvv: \"{}\",expDate: \"{}/{}\",amount: {},currency: \"{}\"}};", - ccard.card_number.peek(), - 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 card_details = + if let Some(api::PaymentMethodData::Card(ccard)) = payment_method_data { + format!( + "var saveCardDirectly={{cvv: \"{}\",amount: {},currency: \"{}\"}};", + ccard.card_cvc.peek(), + amount, + currency + ) + } else { + "".to_string() + }; + 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!(""))) + (PreEscaped(format!(""))) } 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); "))) }}