mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat: 3ds2 Integration (#35)
This commit is contained in:
@ -304,24 +304,11 @@ impl
|
|||||||
data: &types::PaymentsRouterData,
|
data: &types::PaymentsRouterData,
|
||||||
res: Response,
|
res: Response,
|
||||||
) -> CustomResult<types::PaymentsRouterData, errors::ConnectorError> {
|
) -> CustomResult<types::PaymentsRouterData, errors::ConnectorError> {
|
||||||
let response = match data.payment_method {
|
let response: adyen::AdyenPaymentResponse = res
|
||||||
types::storage::enums::PaymentMethodType::Wallet => {
|
|
||||||
let response: adyen::AdyenWalletResponse = res
|
|
||||||
.response
|
.response
|
||||||
.parse_struct("AdyenWalletResponse")
|
.parse_struct("AdyenPaymentResponse")
|
||||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
.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 {
|
types::RouterData::try_from(types::ResponseRouterData {
|
||||||
response,
|
response,
|
||||||
data: data.clone(),
|
data: data.clone(),
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use error_stack::{IntoReport, ResultExt};
|
use error_stack::{IntoReport, ResultExt};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -29,7 +31,7 @@ pub enum AdyenRecurringModel {
|
|||||||
UnscheduledCardOnFile,
|
UnscheduledCardOnFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AdyenPaymentRequest {
|
pub struct AdyenPaymentRequest {
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
@ -37,11 +39,25 @@ pub struct AdyenPaymentRequest {
|
|||||||
payment_method: AdyenPaymentMethod,
|
payment_method: AdyenPaymentMethod,
|
||||||
reference: String,
|
reference: String,
|
||||||
return_url: String,
|
return_url: String,
|
||||||
|
browser_info: Option<AdyenBrowserInfo>,
|
||||||
shopper_interaction: AdyenShopperInteraction,
|
shopper_interaction: AdyenShopperInteraction,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
recurring_processing_model: Option<AdyenRecurringModel>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
pub struct AdyenRedirectRequest {
|
pub struct AdyenRedirectRequest {
|
||||||
pub details: AdyenRedirectRequestTypes,
|
pub details: AdyenRedirectRequestTypes,
|
||||||
@ -78,7 +94,7 @@ pub struct AdyenThreeDS {
|
|||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum AdyenPaymentResponse {
|
pub enum AdyenPaymentResponse {
|
||||||
AdyenResponse(AdyenResponse),
|
AdyenResponse(AdyenResponse),
|
||||||
AdyenWalletResponse(AdyenWalletResponse),
|
AdyenRedirectResponse(AdyenWalletResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -109,6 +125,7 @@ pub struct AdyenWalletAction {
|
|||||||
method: String,
|
method: String,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
type_of_response: String,
|
type_of_response: String,
|
||||||
|
data: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
#[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
|
// Payment Request Transform
|
||||||
impl TryFrom<&types::PaymentsRouterData> for AdyenPaymentRequest {
|
impl TryFrom<&types::PaymentsRouterData> for AdyenPaymentRequest {
|
||||||
type Error = error_stack::Report<errors::ConnectorError>;
|
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 {
|
Ok(AdyenPaymentRequest {
|
||||||
amount,
|
amount,
|
||||||
merchant_account: auth_type.merchant_account,
|
merchant_account: auth_type.merchant_account,
|
||||||
payment_method,
|
payment_method,
|
||||||
reference,
|
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,
|
shopper_interaction,
|
||||||
recurring_processing_model,
|
recurring_processing_model,
|
||||||
|
browser_info,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -379,12 +432,10 @@ pub fn get_wallet_response(
|
|||||||
|
|
||||||
let redirection_data = services::RedirectForm {
|
let redirection_data = services::RedirectForm {
|
||||||
url: redirection_url_response.to_string(),
|
url: redirection_url_response.to_string(),
|
||||||
method: services::Method::Get,
|
method: services::Method::from_str(&response.action.method)
|
||||||
form_fields: std::collections::HashMap::from_iter(
|
.into_report()
|
||||||
redirection_url_response
|
.change_context(errors::ParsingError)?,
|
||||||
.query_pairs()
|
form_fields: response.action.data,
|
||||||
.map(|(k, v)| (k.to_string(), v.to_string())),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let payments_response_data = types::PaymentsResponseData {
|
let payments_response_data = types::PaymentsResponseData {
|
||||||
@ -405,7 +456,7 @@ impl<F, Req>
|
|||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
let (status, error, payment_response_data) = match item.response {
|
let (status, error, payment_response_data) = match item.response {
|
||||||
AdyenPaymentResponse::AdyenResponse(response) => get_adyen_response(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 {
|
Ok(types::RouterData {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ use crate::{
|
|||||||
enums::{self, IntentStatus},
|
enums::{self, IntentStatus},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
utils::OptionExt,
|
utils::{self, OptionExt},
|
||||||
};
|
};
|
||||||
#[derive(Debug, Clone, Copy, PaymentOperation)]
|
#[derive(Debug, Clone, Copy, PaymentOperation)]
|
||||||
#[operation(ops = "all", flow = "authorize")]
|
#[operation(ops = "all", flow = "authorize")]
|
||||||
@ -70,6 +70,15 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
|
|||||||
let billing_address =
|
let billing_address =
|
||||||
helpers::get_address_for_payment_request(db, request.billing.as_ref(), None).await?;
|
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
|
payment_attempt = match db
|
||||||
.insert_payment_attempt(Self::make_payment_attempt(
|
.insert_payment_attempt(Self::make_payment_attempt(
|
||||||
&payment_id,
|
&payment_id,
|
||||||
@ -78,6 +87,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
|
|||||||
money,
|
money,
|
||||||
payment_method_type,
|
payment_method_type,
|
||||||
request,
|
request,
|
||||||
|
browser_info,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@ -287,6 +297,7 @@ impl PaymentCreate {
|
|||||||
money: (i32, enums::Currency),
|
money: (i32, enums::Currency),
|
||||||
payment_method: Option<enums::PaymentMethodType>,
|
payment_method: Option<enums::PaymentMethodType>,
|
||||||
request: &api::PaymentsRequest,
|
request: &api::PaymentsRequest,
|
||||||
|
browser_info: Option<serde_json::Value>,
|
||||||
) -> storage::PaymentAttemptNew {
|
) -> storage::PaymentAttemptNew {
|
||||||
let created_at @ modified_at @ last_synced = Some(crate::utils::date_time::now());
|
let created_at @ modified_at @ last_synced = Some(crate::utils::date_time::now());
|
||||||
let status =
|
let status =
|
||||||
@ -308,6 +319,7 @@ impl PaymentCreate {
|
|||||||
modified_at,
|
modified_at,
|
||||||
last_synced,
|
last_synced,
|
||||||
authentication_type: request.authentication_type,
|
authentication_type: request.authentication_type,
|
||||||
|
browser_info,
|
||||||
..storage::PaymentAttemptNew::default()
|
..storage::PaymentAttemptNew::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -255,6 +255,14 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsRequestData {
|
|||||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||||
|
|
||||||
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
|
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 {
|
Ok(Self {
|
||||||
payment_method_data: {
|
payment_method_data: {
|
||||||
let payment_method_type = payment_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,
|
confirm: payment_data.payment_attempt.confirm,
|
||||||
statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix,
|
statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix,
|
||||||
capture_method: payment_data.payment_attempt.capture_method,
|
capture_method: payment_data.payment_attempt.capture_method,
|
||||||
|
browser_info,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -200,6 +200,7 @@ mod storage {
|
|||||||
amount_to_capture: payment_attempt.amount_to_capture,
|
amount_to_capture: payment_attempt.amount_to_capture,
|
||||||
cancellation_reason: payment_attempt.cancellation_reason.clone(),
|
cancellation_reason: payment_attempt.cancellation_reason.clone(),
|
||||||
mandate_id: payment_attempt.mandate_id.clone(),
|
mandate_id: payment_attempt.mandate_id.clone(),
|
||||||
|
browser_info: payment_attempt.browser_info.clone(),
|
||||||
};
|
};
|
||||||
// TODO: Add a proper error for serialization failure
|
// TODO: Add a proper error for serialization failure
|
||||||
let redis_value = serde_json::to_string(&created_attempt)
|
let redis_value = serde_json::to_string(&created_attempt)
|
||||||
|
|||||||
@ -203,6 +203,7 @@ diesel::table! {
|
|||||||
cancellation_reason -> Nullable<Varchar>,
|
cancellation_reason -> Nullable<Varchar>,
|
||||||
amount_to_capture -> Nullable<Int4>,
|
amount_to_capture -> Nullable<Int4>,
|
||||||
mandate_id -> Nullable<Varchar>,
|
mandate_id -> Nullable<Varchar>,
|
||||||
|
browser_info -> Nullable<Jsonb>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,9 @@ use crate::{
|
|||||||
|
|
||||||
pub(crate) type Headers = Vec<(String, String)>;
|
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")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
#[strum(serialize_all = "UPPERCASE")]
|
#[strum(serialize_all = "UPPERCASE")]
|
||||||
pub enum Method {
|
pub enum Method {
|
||||||
|
|||||||
@ -75,6 +75,7 @@ pub struct PaymentsRequestData {
|
|||||||
pub mandate_id: Option<String>,
|
pub mandate_id: Option<String>,
|
||||||
pub off_session: Option<bool>,
|
pub off_session: Option<bool>,
|
||||||
pub setup_mandate_details: Option<payments::MandateData>,
|
pub setup_mandate_details: Option<payments::MandateData>,
|
||||||
|
pub browser_info: Option<BrowserInformation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -112,7 +113,7 @@ pub struct RefundsRequestData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct DeviceInformation {
|
pub struct BrowserInformation {
|
||||||
pub color_depth: u8,
|
pub color_depth: u8,
|
||||||
pub java_enabled: bool,
|
pub java_enabled: bool,
|
||||||
pub java_script_enabled: bool,
|
pub java_script_enabled: bool,
|
||||||
|
|||||||
@ -50,6 +50,7 @@ pub struct PaymentsRequest {
|
|||||||
pub payment_token: Option<i32>,
|
pub payment_token: Option<i32>,
|
||||||
pub shipping: Option<Address>,
|
pub shipping: Option<Address>,
|
||||||
pub billing: Option<Address>,
|
pub billing: Option<Address>,
|
||||||
|
pub browser_info: Option<types::BrowserInformation>,
|
||||||
pub statement_descriptor_name: Option<String>,
|
pub statement_descriptor_name: Option<String>,
|
||||||
pub statement_descriptor_suffix: Option<String>,
|
pub statement_descriptor_suffix: Option<String>,
|
||||||
pub metadata: Option<serde_json::Value>,
|
pub metadata: Option<serde_json::Value>,
|
||||||
|
|||||||
@ -35,6 +35,7 @@ pub struct PaymentAttempt {
|
|||||||
pub cancellation_reason: Option<String>,
|
pub cancellation_reason: Option<String>,
|
||||||
pub amount_to_capture: Option<i32>,
|
pub amount_to_capture: Option<i32>,
|
||||||
pub mandate_id: Option<String>,
|
pub mandate_id: Option<String>,
|
||||||
|
pub browser_info: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Insertable, router_derive::DebugAsDisplay)]
|
#[derive(Clone, Debug, Default, Insertable, router_derive::DebugAsDisplay)]
|
||||||
@ -68,6 +69,7 @@ pub struct PaymentAttemptNew {
|
|||||||
pub cancellation_reason: Option<String>,
|
pub cancellation_reason: Option<String>,
|
||||||
pub amount_to_capture: Option<i32>,
|
pub amount_to_capture: Option<i32>,
|
||||||
pub mandate_id: Option<String>,
|
pub mandate_id: Option<String>,
|
||||||
|
pub browser_info: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@ -46,6 +46,7 @@ fn construct_payment_router_data() -> types::PaymentsRouterData {
|
|||||||
off_session: None,
|
off_session: None,
|
||||||
setup_mandate_details: None,
|
setup_mandate_details: None,
|
||||||
capture_method: None,
|
capture_method: None,
|
||||||
|
browser_info: None,
|
||||||
},
|
},
|
||||||
response: None,
|
response: None,
|
||||||
payment_method_id: None,
|
payment_method_id: None,
|
||||||
|
|||||||
@ -46,6 +46,7 @@ fn construct_payment_router_data() -> types::PaymentsRouterData {
|
|||||||
off_session: None,
|
off_session: None,
|
||||||
setup_mandate_details: None,
|
setup_mandate_details: None,
|
||||||
capture_method: None,
|
capture_method: None,
|
||||||
|
browser_info: None,
|
||||||
},
|
},
|
||||||
payment_method_id: None,
|
payment_method_id: None,
|
||||||
response: None,
|
response: None,
|
||||||
|
|||||||
@ -42,6 +42,7 @@ fn construct_payment_router_data() -> types::PaymentsRouterData {
|
|||||||
off_session: None,
|
off_session: None,
|
||||||
setup_mandate_details: None,
|
setup_mandate_details: None,
|
||||||
capture_method: None,
|
capture_method: None,
|
||||||
|
browser_info: None,
|
||||||
},
|
},
|
||||||
response: None,
|
response: None,
|
||||||
payment_method_id: None,
|
payment_method_id: None,
|
||||||
|
|||||||
@ -326,6 +326,7 @@ async fn payments_create_core() {
|
|||||||
mandate_id: None,
|
mandate_id: None,
|
||||||
off_session: None,
|
off_session: None,
|
||||||
client_secret: None,
|
client_secret: None,
|
||||||
|
browser_info: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let expected_response = api::PaymentsResponse {
|
let expected_response = api::PaymentsResponse {
|
||||||
@ -484,6 +485,7 @@ async fn payments_create_core_adyen_no_redirect() {
|
|||||||
mandate_id: None,
|
mandate_id: None,
|
||||||
off_session: None,
|
off_session: None,
|
||||||
client_secret: None,
|
client_secret: None,
|
||||||
|
browser_info: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let expected_response = services::BachResponse::Json(api::PaymentsResponse {
|
let expected_response = services::BachResponse::Json(api::PaymentsResponse {
|
||||||
|
|||||||
@ -91,6 +91,7 @@ async fn payments_create_core() {
|
|||||||
mandate_id: None,
|
mandate_id: None,
|
||||||
off_session: None,
|
off_session: None,
|
||||||
client_secret: None,
|
client_secret: None,
|
||||||
|
browser_info: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let expected_response = api::PaymentsResponse {
|
let expected_response = api::PaymentsResponse {
|
||||||
@ -252,6 +253,7 @@ async fn payments_create_core_adyen_no_redirect() {
|
|||||||
off_session: None,
|
off_session: None,
|
||||||
mandate_id: None,
|
mandate_id: None,
|
||||||
client_secret: None,
|
client_secret: None,
|
||||||
|
browser_info: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let expected_response = services::BachResponse::Json(api::PaymentsResponse {
|
let expected_response = services::BachResponse::Json(api::PaymentsResponse {
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE payment_attempt
|
||||||
|
DROP COLUMN browser_info;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE payment_attempt
|
||||||
|
ADD COLUMN browser_info JSONB DEFAULT '{}'::JSONB;
|
||||||
Reference in New Issue
Block a user