From 20bea23b75c30b27f5beda78ac2ffa8302c6e6a8 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Tue, 25 Apr 2023 01:06:12 +0530 Subject: [PATCH] feat(connector): add 3ds for Bambora and Support Html 3ds response (#817) Co-authored-by: Narayan Bhat Co-authored-by: shankar singh Co-authored-by: Jagan Elavarasan Co-authored-by: Arun Raj M --- .../src/connector/adyen/transformers.rs | 2 +- .../src/connector/airwallex/transformers.rs | 10 +- crates/router/src/connector/bambora.rs | 127 +++++++++- .../src/connector/bambora/transformers.rs | 223 +++++++++++++----- .../src/connector/coinbase/transformers.rs | 2 +- .../src/connector/nuvei/transformers.rs | 2 +- .../src/connector/opennode/transformers.rs | 2 +- .../src/connector/trustpay/transformers.rs | 12 +- crates/router/src/core/payments/flows.rs | 2 - .../router/src/core/payments/transformers.rs | 2 +- crates/router/src/services/api.rs | 28 ++- 11 files changed, 325 insertions(+), 87 deletions(-) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index de5932fe63..570ad607f0 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -1324,7 +1324,7 @@ pub fn get_redirection_response( .map(|(key, value)| (key.to_string(), value.to_string())), ) }); - services::RedirectForm { + services::RedirectForm::Form { endpoint: url.to_string(), method: response.action.method.unwrap_or(services::Method::Get), form_fields, diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index c71f2ead3a..9283c7eb70 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -150,7 +150,7 @@ pub struct AirwallexCompleteRequest { #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct AirwallexThreeDsData { - acs_response: Option, + acs_response: Option>, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -166,7 +166,11 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for AirwallexCompleteR Ok(Self { request_id: Uuid::new_v4().to_string(), three_ds: AirwallexThreeDsData { - acs_response: item.request.payload.clone().map(Secret::new), + acs_response: item + .request + .payload + .as_ref() + .map(|data| Secret::new(serde_json::Value::to_string(data))), }, three_ds_type: AirwallexThreeDsType::ThreeDSContinue, }) @@ -285,7 +289,7 @@ pub struct AirwallexPaymentsResponse { fn get_redirection_form( response_url_data: AirwallexPaymentsNextAction, ) -> Option { - Some(services::RedirectForm { + Some(services::RedirectForm::Form { endpoint: response_url_data.url.to_string(), method: response_url_data.method, form_fields: std::collections::HashMap::from([ diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index 59d816899e..984134f46b 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -8,8 +8,11 @@ use transformers as bambora; use super::utils::RefundsRequestData; use crate::{ configs::settings, - connector::utils::{PaymentsAuthorizeRequestData, PaymentsSyncRequestData}, - core::errors::{self, CustomResult}, + connector::utils::{to_connector_meta, PaymentsAuthorizeRequestData, PaymentsSyncRequestData}, + core::{ + errors::{self, CustomResult}, + payments, + }, headers, logger, services::{self, ConnectorIntegration}, types::{ @@ -166,7 +169,7 @@ impl ConnectorIntegration CustomResult { - let response: bambora::BamboraPaymentsResponse = res + let response: bambora::BamboraResponse = res .response .parse_struct("bambora PaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -257,7 +260,7 @@ impl ConnectorIntegration CustomResult { - let response: bambora::BamboraPaymentsResponse = res + let response: bambora::BamboraResponse = res .response .parse_struct("bambora PaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -336,7 +339,7 @@ impl ConnectorIntegration CustomResult { - let response: bambora::BamboraPaymentsResponse = res + let response: bambora::BamboraResponse = res .response .parse_struct("Bambora PaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -388,9 +391,9 @@ impl ConnectorIntegration CustomResult { - Ok(format!("{}{}", self.base_url(_connectors), "/v1/payments")) + Ok(format!("{}{}", self.base_url(connectors), "/v1/payments")) } fn get_request_body( @@ -429,7 +432,7 @@ impl ConnectorIntegration CustomResult { - let response: bambora::BamboraPaymentsResponse = res + let response: bambora::BamboraResponse = res .response .parse_struct("PaymentIntentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -638,3 +641,111 @@ pub fn get_payment_flow(is_auto_capture: bool) -> bambora::PaymentFlow { bambora::PaymentFlow::Authorize } } + +impl services::ConnectorRedirectResponse for Bambora { + fn get_flow_type( + &self, + _query_params: &str, + _json_payload: Option, + _action: services::PaymentAction, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} + +impl api::PaymentsCompleteAuthorize for Bambora {} + +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Bambora +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let meta: bambora::BamboraMeta = to_connector_meta(req.request.connector_meta.clone())?; + Ok(format!( + "{}/v1/payments/{}{}", + self.base_url(connectors), + meta.three_d_session_data, + "/continue" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let request = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?; + let bambora_req = + utils::Encode::::encode_to_string_of_json( + &request, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bambora_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: bambora::BamboraResponse = res + .response + .parse_struct("Bambora PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(bamborapayments_create_response=?response); + types::RouterData::try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + bambora::PaymentFlow::Capture, + )) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index f9dbf97f19..70f6d231a0 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -1,4 +1,6 @@ use base64::Engine; +use common_utils::ext_traits::ValueExt; +use error_stack::{IntoReport, ResultExt}; use masking::Secret; use serde::{Deserialize, Deserializer, Serialize}; @@ -6,6 +8,7 @@ use crate::{ connector::utils::PaymentsAuthorizeRequestData, consts, core::errors, + services, types::{self, api, storage::enums}, }; @@ -24,19 +27,21 @@ pub struct BamboraCard { #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct ThreeDSecure { - // browser: Option, //Needed only in case of 3Ds 2.0. Need to update request for this. + browser: Option, //Needed only in case of 3Ds 2.0. Need to update request for this. enabled: bool, + version: Option, + auth_required: Option, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct Browser { +pub struct BamboraBrowserInfo { accept_header: String, - java_enabled: String, + java_enabled: bool, language: String, - color_depth: String, - screen_height: i64, - screen_width: i64, - time_zone: i64, + color_depth: u8, + screen_height: u32, + screen_width: u32, + time_zone: i32, user_agent: String, javascript_enabled: bool, } @@ -45,16 +50,63 @@ pub struct Browser { pub struct BamboraPaymentsRequest { amount: i64, payment_method: PaymentMethod, + customer_ip: Option, + term_url: Option, card: BamboraCard, } +fn get_browser_info(item: &types::PaymentsAuthorizeRouterData) -> Option { + if matches!(item.auth_type, enums::AuthenticationType::ThreeDs) { + item.request + .browser_info + .as_ref() + .map(|info| BamboraBrowserInfo { + accept_header: info.accept_header.clone(), + java_enabled: info.java_enabled, + language: info.language.clone(), + color_depth: info.color_depth, + screen_height: info.screen_height, + screen_width: info.screen_width, + time_zone: info.time_zone, + user_agent: info.user_agent.clone(), + javascript_enabled: info.java_script_enabled, + }) + } else { + None + } +} + +impl TryFrom<&types::CompleteAuthorizeData> for BamboraThreedsContinueRequest { + type Error = error_stack::Report; + fn try_from(value: &types::CompleteAuthorizeData) -> Result { + let card_response: CardResponse = value + .payload + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "payload", + })? + .parse_value("CardResponse") + .change_context(errors::ConnectorError::ParsingFailed)?; + let bambora_req = Self { + payment_method: "credit_card".to_string(), + card_response, + }; + Ok(bambora_req) + } +} + impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => { let three_ds = match item.auth_type { - enums::AuthenticationType::ThreeDs => Some(ThreeDSecure { enabled: true }), + enums::AuthenticationType::ThreeDs => Some(ThreeDSecure { + enabled: true, + browser: get_browser_info(item), + version: Some(2), + auth_required: Some(true), + }), enums::AuthenticationType::NoThreeDs => None, }; let bambora_card = BamboraCard { @@ -66,10 +118,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest { three_d_secure: three_ds, complete: item.request.is_auto_capture()?, }; + let browser_info = item.request.get_browser_info()?; Ok(Self { amount: item.request.amount, payment_method: PaymentMethod::Card, card: bambora_card, + customer_ip: browser_info.ip_address, + term_url: item.request.complete_authorize_url.clone(), }) } _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), @@ -115,42 +170,68 @@ pub enum PaymentFlow { // PaymentsResponse impl TryFrom<( - types::ResponseRouterData, + types::ResponseRouterData, PaymentFlow, )> for types::RouterData { type Error = error_stack::Report; fn try_from( data: ( - types::ResponseRouterData, + types::ResponseRouterData, PaymentFlow, ), ) -> Result { let flow = data.1; let item = data.0; - let pg_response = item.response; - Ok(Self { - status: match pg_response.approved.as_str() { - "0" => match flow { - PaymentFlow::Authorize => enums::AttemptStatus::AuthorizationFailed, - PaymentFlow::Capture => enums::AttemptStatus::Failure, - PaymentFlow::Void => enums::AttemptStatus::VoidFailed, + match item.response { + BamboraResponse::NormalTransaction(pg_response) => Ok(Self { + status: match pg_response.approved.as_str() { + "0" => match flow { + PaymentFlow::Authorize => enums::AttemptStatus::AuthorizationFailed, + PaymentFlow::Capture => enums::AttemptStatus::Failure, + PaymentFlow::Void => enums::AttemptStatus::VoidFailed, + }, + "1" => match flow { + PaymentFlow::Authorize => enums::AttemptStatus::Authorized, + PaymentFlow::Capture => enums::AttemptStatus::Charged, + PaymentFlow::Void => enums::AttemptStatus::Voided, + }, + &_ => Err(errors::ConnectorError::ResponseDeserializationFailed)?, }, - "1" => match flow { - PaymentFlow::Authorize => enums::AttemptStatus::Authorized, - PaymentFlow::Capture => enums::AttemptStatus::Charged, - PaymentFlow::Void => enums::AttemptStatus::Voided, - }, - &_ => Err(errors::ConnectorError::ResponseDeserializationFailed)?, - }, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(pg_response.id.to_string()), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + pg_response.id.to_string(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }), + ..item.data }), - ..item.data - }) + + BamboraResponse::ThreeDsResponse(response) => { + let value = url::form_urlencoded::parse(response.contents.as_bytes()) + .map(|(key, val)| [key, val].concat()) + .collect(); + let redirection_data = Some(services::RedirectForm::Html { html_data: value }); + Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data, + mandate_reference: None, + connector_metadata: Some( + serde_json::to_value(BamboraMeta { + three_d_session_data: response.three_d_session_data, + }) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?, + ), + }), + ..item.data + }) + } + } } } @@ -173,7 +254,14 @@ where Ok(res) } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +pub enum BamboraResponse { + NormalTransaction(Box), + ThreeDsResponse(Box), +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] pub struct BamboraPaymentsResponse { #[serde(deserialize_with = "str_or_i32")] id: String, @@ -203,7 +291,30 @@ pub struct BamboraPaymentsResponse { risk_score: Option, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Deserialize)] +pub struct Bambora3DsResponse { + #[serde(rename = "3d_session_data")] + three_d_session_data: String, + contents: String, +} + +#[derive(Debug, Serialize, Default, Deserialize)] +pub struct BamboraMeta { + pub three_d_session_data: String, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct BamboraThreedsContinueRequest { + pub(crate) payment_method: String, + pub card_response: CardResponse, +} + +#[derive(Default, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct CardResponse { + pub(crate) cres: Option, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] pub struct CardData { name: Option, expiry_month: Option, @@ -337,34 +448,34 @@ impl From for enums::RefundStatus { } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Deserialize)] pub struct RefundResponse { #[serde(deserialize_with = "str_or_i32")] - id: String, - authorizing_merchant_id: i32, + pub id: String, + pub authorizing_merchant_id: i32, #[serde(deserialize_with = "str_or_i32")] - approved: String, + pub approved: String, #[serde(deserialize_with = "str_or_i32")] - message_id: String, - message: String, - auth_code: String, - created: String, - amount: f32, - order_number: String, + pub message_id: String, + pub message: String, + pub auth_code: String, + pub created: String, + pub amount: f32, + pub order_number: String, #[serde(rename = "type")] - payment_type: String, - comments: Option, - batch_number: Option, - total_refunds: Option, - total_completions: Option, - payment_method: String, - card: CardData, - billing: Option, - shipping: Option, - custom: CustomData, - adjusted_by: Option>, - links: Vec, - risk_score: Option, + pub payment_type: String, + pub comments: Option, + pub batch_number: Option, + pub total_refunds: Option, + pub total_completions: Option, + pub payment_method: String, + pub card: CardData, + pub billing: Option, + pub shipping: Option, + pub custom: CustomData, + pub adjusted_by: Option>, + pub links: Vec, + pub risk_score: Option, } impl TryFrom> diff --git a/crates/router/src/connector/coinbase/transformers.rs b/crates/router/src/connector/coinbase/transformers.rs index fcd8cd24b2..627ae4dadd 100644 --- a/crates/router/src/connector/coinbase/transformers.rs +++ b/crates/router/src/connector/coinbase/transformers.rs @@ -124,7 +124,7 @@ impl >, ) -> Result { let form_fields = HashMap::new(); - let redirection_data = services::RedirectForm { + let redirection_data = services::RedirectForm::Form { endpoint: item.response.data.hosted_url.to_string(), method: services::Method::Get, form_fields, diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 2d125818b7..2207cac17e 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1121,7 +1121,7 @@ where .and_then(|o| o.card.clone()) .and_then(|card| card.three_d) .and_then(|three_ds| three_ds.acs_url.zip(three_ds.c_req)) - .map(|(base_url, creq)| services::RedirectForm { + .map(|(base_url, creq)| services::RedirectForm::Form { endpoint: base_url, method: services::Method::Post, form_fields: std::collections::HashMap::from([("creq".to_string(), creq)]), diff --git a/crates/router/src/connector/opennode/transformers.rs b/crates/router/src/connector/opennode/transformers.rs index f951b8eb92..e2c9e84090 100644 --- a/crates/router/src/connector/opennode/transformers.rs +++ b/crates/router/src/connector/opennode/transformers.rs @@ -97,7 +97,7 @@ impl >, ) -> Result { let form_fields = HashMap::new(); - let redirection_data = services::RedirectForm { + let redirection_data = services::RedirectForm::Form { endpoint: item.response.data.hosted_checkout_url.to_string(), method: services::Method::Get, form_fields, diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index f039321070..ae63a7277a 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -558,11 +558,13 @@ fn handle_cards_response( response.redirect_url.clone(), )?; let form_fields = response.redirect_params.unwrap_or_default(); - let redirection_data = response.redirect_url.map(|url| services::RedirectForm { - endpoint: url.to_string(), - method: services::Method::Post, - form_fields, - }); + let redirection_data = response + .redirect_url + .map(|url| services::RedirectForm::Form { + endpoint: url.to_string(), + method: services::Method::Post, + form_fields, + }); let error = if msg.is_some() { Some(types::ErrorResponse { code: response.payment_status, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index f1c50f3e78..945351994b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -89,7 +89,6 @@ default_imp_for_complete_authorize!( connector::Aci, connector::Adyen, connector::Authorizedotnet, - connector::Bambora, connector::Bluesnap, connector::Braintree, connector::Checkout, @@ -132,7 +131,6 @@ default_imp_for_connector_redirect_response!( connector::Aci, connector::Adyen, connector::Authorizedotnet, - connector::Bambora, connector::Bluesnap, connector::Braintree, connector::Coinbase, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index c465f68360..acbca0f48c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -701,7 +701,7 @@ impl TryFrom> for types::CompleteAuthoriz let json_payload = payment_data .connector_response .encoded_data - .map(serde_json::to_value) + .map(|s| serde_json::from_str::(&s)) .transpose() .into_report() .change_context(errors::ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index aa4bdd9c49..13080cf8ad 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -467,10 +467,15 @@ pub struct ApplicationRedirectResponse { } #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] -pub struct RedirectForm { - pub endpoint: String, - pub method: Method, - pub form_fields: HashMap, +pub enum RedirectForm { + Form { + endpoint: String, + method: Method, + form_fields: HashMap, + }, + Html { + html_data: String, + }, } impl From<(url::Url, Method)> for RedirectForm { @@ -484,7 +489,7 @@ impl From<(url::Url, Method)> for RedirectForm { // Do not include query params in the endpoint redirect_url.set_query(None); - Self { + Self::Form { endpoint: redirect_url.to_string(), method, form_fields, @@ -681,7 +686,12 @@ impl Authenticate for api_models::payment_methods::PaymentMethodListRequest { pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup { use maud::PreEscaped; - maud::html! { + match form { + RedirectForm::Form { + endpoint, + method, + form_fields, + } => maud::html! { (maud::DOCTYPE) html { meta name="viewport" content="width=device-width, initial-scale=1"; @@ -726,8 +736,8 @@ pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup { h3 style="text-align: center;" { "Please wait while we process your payment..." } - form action=(PreEscaped(&form.endpoint)) method=(form.method.to_string()) #payment_form { - @for (field, value) in &form.form_fields { + form action=(PreEscaped(endpoint)) method=(method.to_string()) #payment_form { + @for (field, value) in form_fields { input type="hidden" name=(field) value=(value); } } @@ -735,6 +745,8 @@ pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup { (PreEscaped(r#""#)) } } + }, + RedirectForm::Html { html_data } => PreEscaped(html_data.to_string()), } }