feat: 3ds2 Integration (#35)

This commit is contained in:
Kartikeya Hegde
2022-11-29 15:27:35 +05:30
committed by GitHub
parent d67bd801f6
commit 54c3fc349d
17 changed files with 108 additions and 30 deletions

View File

@ -304,24 +304,11 @@ impl
data: &types::PaymentsRouterData,
res: Response,
) -> CustomResult<types::PaymentsRouterData, errors::ConnectorError> {
let response = match data.payment_method {
types::storage::enums::PaymentMethodType::Wallet => {
let response: adyen::AdyenWalletResponse = res
.response
.parse_struct("AdyenWalletResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response: adyen::AdyenPaymentResponse = res
.response
.parse_struct("AdyenPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
adyen::AdyenPaymentResponse::AdyenWalletResponse(response)
}
_ => {
let response: adyen::AdyenResponse = res
.response
.parse_struct("AdyenResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
adyen::AdyenPaymentResponse::AdyenResponse(response)
}
};
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),

View File

@ -1,3 +1,5 @@
use std::{collections::HashMap, str::FromStr};
use error_stack::{IntoReport, ResultExt};
use reqwest::Url;
use serde::{Deserialize, Serialize};
@ -29,7 +31,7 @@ pub enum AdyenRecurringModel {
UnscheduledCardOnFile,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdyenPaymentRequest {
amount: Amount,
@ -37,11 +39,25 @@ pub struct AdyenPaymentRequest {
payment_method: AdyenPaymentMethod,
reference: String,
return_url: String,
browser_info: Option<AdyenBrowserInfo>,
shopper_interaction: AdyenShopperInteraction,
#[serde(skip_serializing_if = "Option::is_none")]
recurring_processing_model: Option<AdyenRecurringModel>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct AdyenBrowserInfo {
user_agent: String,
accept_header: String,
language: String,
color_depth: u8,
screen_height: u32,
screen_width: u32,
time_zone_offset: i32,
java_enabled: bool,
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct AdyenRedirectRequest {
pub details: AdyenRedirectRequestTypes,
@ -78,7 +94,7 @@ pub struct AdyenThreeDS {
#[serde(untagged)]
pub enum AdyenPaymentResponse {
AdyenResponse(AdyenResponse),
AdyenWalletResponse(AdyenWalletResponse),
AdyenRedirectResponse(AdyenWalletResponse),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -109,6 +125,7 @@ pub struct AdyenWalletAction {
method: String,
#[serde(rename = "type")]
type_of_response: String,
data: HashMap<String, String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
@ -194,6 +211,27 @@ impl TryFrom<&types::ConnectorAuthType> for AdyenAuthType {
}
}
}
impl TryFrom<&types::BrowserInformation> for AdyenBrowserInfo {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::BrowserInformation) -> Result<Self, Self::Error> {
Ok(Self {
accept_header: item.accept_header.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "accept_header".to_string(),
},
)?,
language: item.language.clone(),
screen_height: item.screen_height,
screen_width: item.screen_width,
color_depth: item.color_depth,
user_agent: item.user_agent.clone(),
time_zone_offset: item.time_zone,
java_enabled: item.java_enabled,
})
}
}
// Payment Request Transform
impl TryFrom<&types::PaymentsRouterData> for AdyenPaymentRequest {
type Error = error_stack::Report<errors::ConnectorError>;
@ -253,14 +291,29 @@ impl TryFrom<&types::PaymentsRouterData> for AdyenPaymentRequest {
}),
}?;
let browser_info = if matches!(item.auth_type, enums::AuthenticationType::ThreeDs) {
item.request
.browser_info
.clone()
.map(|d| AdyenBrowserInfo::try_from(&d))
.transpose()?
} else {
None
};
Ok(AdyenPaymentRequest {
amount,
merchant_account: auth_type.merchant_account,
payment_method,
reference,
return_url: "juspay.io".to_string(),
return_url: item.orca_return_url.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "orca_return_url".into(),
},
)?,
shopper_interaction,
recurring_processing_model,
browser_info,
})
}
}
@ -379,12 +432,10 @@ pub fn get_wallet_response(
let redirection_data = services::RedirectForm {
url: redirection_url_response.to_string(),
method: services::Method::Get,
form_fields: std::collections::HashMap::from_iter(
redirection_url_response
.query_pairs()
.map(|(k, v)| (k.to_string(), v.to_string())),
),
method: services::Method::from_str(&response.action.method)
.into_report()
.change_context(errors::ParsingError)?,
form_fields: response.action.data,
};
let payments_response_data = types::PaymentsResponseData {
@ -405,7 +456,7 @@ impl<F, Req>
) -> Result<Self, Self::Error> {
let (status, error, payment_response_data) = match item.response {
AdyenPaymentResponse::AdyenResponse(response) => get_adyen_response(response)?,
AdyenPaymentResponse::AdyenWalletResponse(response) => get_wallet_response(response)?,
AdyenPaymentResponse::AdyenRedirectResponse(response) => get_wallet_response(response)?,
};
Ok(types::RouterData {

View File

@ -26,7 +26,7 @@ use crate::{
enums::{self, IntentStatus},
},
},
utils::OptionExt,
utils::{self, OptionExt},
};
#[derive(Debug, Clone, Copy, PaymentOperation)]
#[operation(ops = "all", flow = "authorize")]
@ -70,6 +70,15 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
let billing_address =
helpers::get_address_for_payment_request(db, request.billing.as_ref(), None).await?;
let browser_info = request
.browser_info
.clone()
.map(|x| utils::Encode::<types::BrowserInformation>::encode_to_value(&x))
.transpose()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "browser_info",
})?;
payment_attempt = match db
.insert_payment_attempt(Self::make_payment_attempt(
&payment_id,
@ -78,6 +87,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
money,
payment_method_type,
request,
browser_info,
))
.await
{
@ -287,6 +297,7 @@ impl PaymentCreate {
money: (i32, enums::Currency),
payment_method: Option<enums::PaymentMethodType>,
request: &api::PaymentsRequest,
browser_info: Option<serde_json::Value>,
) -> storage::PaymentAttemptNew {
let created_at @ modified_at @ last_synced = Some(crate::utils::date_time::now());
let status =
@ -308,6 +319,7 @@ impl PaymentCreate {
modified_at,
last_synced,
authentication_type: request.authentication_type,
browser_info,
..storage::PaymentAttemptNew::default()
}
}

View File

@ -255,6 +255,14 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsRequestData {
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
let browser_info: Option<types::BrowserInformation> = payment_data
.payment_attempt
.browser_info
.map(|b| b.parse_value("BrowserInformation"))
.transpose()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "browser_info",
})?;
Ok(Self {
payment_method_data: {
let payment_method_type = payment_data
@ -276,6 +284,7 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsRequestData {
confirm: payment_data.payment_attempt.confirm,
statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix,
capture_method: payment_data.payment_attempt.capture_method,
browser_info,
})
}
}

View File

@ -200,6 +200,7 @@ mod storage {
amount_to_capture: payment_attempt.amount_to_capture,
cancellation_reason: payment_attempt.cancellation_reason.clone(),
mandate_id: payment_attempt.mandate_id.clone(),
browser_info: payment_attempt.browser_info.clone(),
};
// TODO: Add a proper error for serialization failure
let redis_value = serde_json::to_string(&created_attempt)

View File

@ -203,6 +203,7 @@ diesel::table! {
cancellation_reason -> Nullable<Varchar>,
amount_to_capture -> Nullable<Int4>,
mandate_id -> Nullable<Varchar>,
browser_info -> Nullable<Jsonb>,
}
}

View File

@ -12,7 +12,9 @@ use crate::{
pub(crate) type Headers = Vec<(String, String)>;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, strum::Display)]
#[derive(
Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, strum::Display, strum::EnumString,
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum Method {

View File

@ -75,6 +75,7 @@ pub struct PaymentsRequestData {
pub mandate_id: Option<String>,
pub off_session: Option<bool>,
pub setup_mandate_details: Option<payments::MandateData>,
pub browser_info: Option<BrowserInformation>,
}
#[derive(Debug, Clone)]
@ -112,7 +113,7 @@ pub struct RefundsRequestData {
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DeviceInformation {
pub struct BrowserInformation {
pub color_depth: u8,
pub java_enabled: bool,
pub java_script_enabled: bool,

View File

@ -50,6 +50,7 @@ pub struct PaymentsRequest {
pub payment_token: Option<i32>,
pub shipping: Option<Address>,
pub billing: Option<Address>,
pub browser_info: Option<types::BrowserInformation>,
pub statement_descriptor_name: Option<String>,
pub statement_descriptor_suffix: Option<String>,
pub metadata: Option<serde_json::Value>,

View File

@ -35,6 +35,7 @@ pub struct PaymentAttempt {
pub cancellation_reason: Option<String>,
pub amount_to_capture: Option<i32>,
pub mandate_id: Option<String>,
pub browser_info: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Default, Insertable, router_derive::DebugAsDisplay)]
@ -68,6 +69,7 @@ pub struct PaymentAttemptNew {
pub cancellation_reason: Option<String>,
pub amount_to_capture: Option<i32>,
pub mandate_id: Option<String>,
pub browser_info: Option<serde_json::Value>,
}
#[derive(Clone, Debug)]