mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
feat: 3ds2 Integration (#35)
This commit is contained in:
@ -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(),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -203,6 +203,7 @@ diesel::table! {
|
||||
cancellation_reason -> Nullable<Varchar>,
|
||||
amount_to_capture -> Nullable<Int4>,
|
||||
mandate_id -> Nullable<Varchar>,
|
||||
browser_info -> Nullable<Jsonb>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user