feat(connector): add 3ds for Bambora and Support Html 3ds response (#817)

Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in>
Co-authored-by: shankar singh <shankar.singh@shankar.singh-MacBookPro>
Co-authored-by: Jagan Elavarasan <jaganelavarasan@gmail.com>
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
Shankar Singh C
2023-04-25 01:06:12 +05:30
committed by GitHub
parent bdf1e5147e
commit 20bea23b75
11 changed files with 325 additions and 87 deletions

View File

@ -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,

View File

@ -150,7 +150,7 @@ pub struct AirwallexCompleteRequest {
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct AirwallexThreeDsData {
acs_response: Option<common_utils::pii::SecretSerdeValue>,
acs_response: Option<Secret<String>>,
}
#[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<services::RedirectForm> {
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([

View File

@ -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<api::Void, types::PaymentsCancelData, types::PaymentsR
data: &types::PaymentsCancelRouterData,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
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<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
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<api::Capture, types::PaymentsCaptureData, types::Payme
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
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<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<api::Authorize, types::PaymentsAuthorizeData, types::P
data: &types::PaymentsAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
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<serde_json::Value>,
_action: services::PaymentAction,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
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<Vec<(String, 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::PaymentsCompleteAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<Option<String>, errors::ConnectorError> {
let request = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?;
let bambora_req =
utils::Encode::<bambora::BamboraThreedsContinueRequest>::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<Option<services::Request>, 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<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}

View File

@ -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<Browser>, //Needed only in case of 3Ds 2.0. Need to update request for this.
browser: Option<BamboraBrowserInfo>, //Needed only in case of 3Ds 2.0. Need to update request for this.
enabled: bool,
version: Option<i64>,
auth_required: Option<bool>,
}
#[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<std::net::IpAddr>,
term_url: Option<String>,
card: BamboraCard,
}
fn get_browser_info(item: &types::PaymentsAuthorizeRouterData) -> Option<BamboraBrowserInfo> {
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<errors::ConnectorError>;
fn try_from(value: &types::CompleteAuthorizeData) -> Result<Self, Self::Error> {
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<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
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<F, T>
TryFrom<(
types::ResponseRouterData<F, BamboraPaymentsResponse, T, types::PaymentsResponseData>,
types::ResponseRouterData<F, BamboraResponse, T, types::PaymentsResponseData>,
PaymentFlow,
)> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
data: (
types::ResponseRouterData<F, BamboraPaymentsResponse, T, types::PaymentsResponseData>,
types::ResponseRouterData<F, BamboraResponse, T, types::PaymentsResponseData>,
PaymentFlow,
),
) -> Result<Self, Self::Error> {
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<BamboraPaymentsResponse>),
ThreeDsResponse(Box<Bambora3DsResponse>),
}
#[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<f32>,
}
#[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<common_utils::pii::SecretSerdeValue>,
}
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
pub struct CardData {
name: Option<String>,
expiry_month: Option<String>,
@ -337,34 +448,34 @@ impl From<RefundStatus> 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<String>,
batch_number: Option<String>,
total_refunds: Option<f32>,
total_completions: Option<f32>,
payment_method: String,
card: CardData,
billing: Option<AddressData>,
shipping: Option<AddressData>,
custom: CustomData,
adjusted_by: Option<Vec<AdjustedBy>>,
links: Vec<Links>,
risk_score: Option<f32>,
pub payment_type: String,
pub comments: Option<String>,
pub batch_number: Option<String>,
pub total_refunds: Option<f32>,
pub total_completions: Option<f32>,
pub payment_method: String,
pub card: CardData,
pub billing: Option<AddressData>,
pub shipping: Option<AddressData>,
pub custom: CustomData,
pub adjusted_by: Option<Vec<AdjustedBy>>,
pub links: Vec<Links>,
pub risk_score: Option<f32>,
}
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>

View File

@ -124,7 +124,7 @@ impl<F, T>
>,
) -> Result<Self, Self::Error> {
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,

View File

@ -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)]),

View File

@ -97,7 +97,7 @@ impl<F, T>
>,
) -> Result<Self, Self::Error> {
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,

View File

@ -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,

View File

@ -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,

View File

@ -701,7 +701,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
let json_payload = payment_data
.connector_response
.encoded_data
.map(serde_json::to_value)
.map(|s| serde_json::from_str::<serde_json::Value>(&s))
.transpose()
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)?;

View File

@ -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<String, String>,
pub enum RedirectForm {
Form {
endpoint: String,
method: Method,
form_fields: HashMap<String, String>,
},
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#"<script type="text/javascript"> var frm = document.getElementById("payment_form"); window.setTimeout(function () { frm.submit(); }, 300); </script>"#))
}
}
},
RedirectForm::Html { html_data } => PreEscaped(html_data.to_string()),
}
}