mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
feat(router): add support for googlepay step up flow (#2744)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Prasunna Soppa <prasunna.soppa@juspay.in> Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Co-authored-by: sai-harsha-vardhan <harsha111hero@gmail.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -9684,6 +9684,23 @@
|
||||
"GoPayRedirection": {
|
||||
"type": "object"
|
||||
},
|
||||
"GooglePayAssuranceDetails": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"card_holder_authenticated",
|
||||
"account_verified"
|
||||
],
|
||||
"properties": {
|
||||
"card_holder_authenticated": {
|
||||
"type": "boolean",
|
||||
"description": "indicates that Cardholder possession validation has been performed"
|
||||
},
|
||||
"account_verified": {
|
||||
"type": "boolean",
|
||||
"description": "indicates that identification and verifications (ID&V) was performed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GooglePayPaymentMethodInfo": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -9698,6 +9715,14 @@
|
||||
"card_details": {
|
||||
"type": "string",
|
||||
"description": "The details of the card"
|
||||
},
|
||||
"assurance_details": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/GooglePayAssuranceDetails"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -9845,6 +9870,11 @@
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"assurance_details_required": {
|
||||
"type": "boolean",
|
||||
"description": "Whether assurance details are required",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -2601,6 +2601,17 @@ pub struct GooglePayPaymentMethodInfo {
|
||||
pub card_network: String,
|
||||
/// The details of the card
|
||||
pub card_details: String,
|
||||
//assurance_details of the card
|
||||
pub assurance_details: Option<GooglePayAssuranceDetails>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GooglePayAssuranceDetails {
|
||||
///indicates that Cardholder possession validation has been performed
|
||||
pub card_holder_authenticated: bool,
|
||||
/// indicates that identification and verifications (ID&V) was performed
|
||||
pub account_verified: bool,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
@ -4098,6 +4109,8 @@ pub struct GpayAllowedMethodsParameters {
|
||||
/// Billing address parameters
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub billing_address_parameters: Option<GpayBillingAddressParameters>,
|
||||
/// Whether assurance details are required
|
||||
pub assurance_details_required: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
|
||||
@ -316,6 +316,7 @@ impl DashboardRequestPayload {
|
||||
],
|
||||
billing_address_required: None,
|
||||
billing_address_parameters: None,
|
||||
assurance_details_required: Some(true),
|
||||
};
|
||||
let allowed_payment_methods = payments::GpayAllowedPaymentMethods {
|
||||
payment_method_type: String::from("CARD"),
|
||||
|
||||
@ -311,6 +311,7 @@ pub enum PaymentAttemptUpdate {
|
||||
unified_message: Option<Option<String>>,
|
||||
connector_transaction_id: Option<String>,
|
||||
payment_method_data: Option<serde_json::Value>,
|
||||
authentication_type: Option<storage_enums::AuthenticationType>,
|
||||
},
|
||||
CaptureUpdate {
|
||||
amount_to_capture: Option<i64>,
|
||||
@ -741,6 +742,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
unified_message,
|
||||
connector_transaction_id,
|
||||
payment_method_data,
|
||||
authentication_type,
|
||||
} => Self {
|
||||
connector: connector.map(Some),
|
||||
status: Some(status),
|
||||
@ -754,6 +756,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
unified_message,
|
||||
connector_transaction_id,
|
||||
payment_method_data,
|
||||
authentication_type,
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self {
|
||||
|
||||
@ -206,6 +206,17 @@ pub struct GooglePayPaymentMethodInfo {
|
||||
pub card_network: String,
|
||||
/// The details of the card
|
||||
pub card_details: String,
|
||||
//assurance_details of the card
|
||||
pub assurance_details: Option<GooglePayAssuranceDetails>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GooglePayAssuranceDetails {
|
||||
///indicates that Cardholder possession validation has been performed
|
||||
pub card_holder_authenticated: bool,
|
||||
/// indicates that identification and verifications (ID&V) was performed
|
||||
pub account_verified: bool,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
@ -623,6 +634,12 @@ impl From<api_models::payments::GooglePayWalletData> for GooglePayWalletData {
|
||||
info: GooglePayPaymentMethodInfo {
|
||||
card_network: value.info.card_network,
|
||||
card_details: value.info.card_details,
|
||||
assurance_details: value.info.assurance_details.map(|info| {
|
||||
GooglePayAssuranceDetails {
|
||||
card_holder_authenticated: info.card_holder_authenticated,
|
||||
account_verified: info.account_verified,
|
||||
}
|
||||
}),
|
||||
},
|
||||
tokenization_data: GpayTokenizationData {
|
||||
token_type: value.tokenization_data.token_type,
|
||||
|
||||
@ -414,6 +414,7 @@ pub enum PaymentAttemptUpdate {
|
||||
unified_message: Option<Option<String>>,
|
||||
connector_transaction_id: Option<String>,
|
||||
payment_method_data: Option<serde_json::Value>,
|
||||
authentication_type: Option<storage_enums::AuthenticationType>,
|
||||
},
|
||||
CaptureUpdate {
|
||||
amount_to_capture: Option<MinorUnit>,
|
||||
|
||||
@ -493,6 +493,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
api_models::payments::RetrievePaymentLinkResponse,
|
||||
api_models::payments::PaymentLinkInitiateRequest,
|
||||
api_models::payments::ExtendedCardInfoResponse,
|
||||
api_models::payments::GooglePayAssuranceDetails,
|
||||
api_models::routing::RoutingConfigRequest,
|
||||
api_models::routing::RoutingDictionaryRecord,
|
||||
api_models::routing::RoutingKind,
|
||||
|
||||
@ -1123,6 +1123,7 @@ pub struct GpayTokenizationSpecification {
|
||||
pub struct GpayAllowedMethodsParameters {
|
||||
pub allowed_auth_methods: Vec<String>,
|
||||
pub allowed_card_networks: Vec<String>,
|
||||
pub assurance_details_required: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Deserialize, Serialize)]
|
||||
@ -1343,6 +1344,7 @@ impl From<GpayAllowedMethodsParameters> for api_models::payments::GpayAllowedMet
|
||||
allowed_card_networks: value.allowed_card_networks,
|
||||
billing_address_required: None,
|
||||
billing_address_parameters: None,
|
||||
assurance_details_required: value.assurance_details_required,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,6 +246,20 @@ pub trait RouterDataAuthorize {
|
||||
|
||||
impl RouterDataAuthorize for types::PaymentsAuthorizeRouterData {
|
||||
fn decide_authentication_type(&mut self) {
|
||||
if let hyperswitch_domain_models::payment_method_data::PaymentMethodData::Wallet(
|
||||
hyperswitch_domain_models::payment_method_data::WalletData::GooglePay(google_pay_data),
|
||||
) = &self.request.payment_method_data
|
||||
{
|
||||
if let Some(assurance_details) = google_pay_data.info.assurance_details.as_ref() {
|
||||
// Step up the transaction to 3DS when either assurance_details.card_holder_authenticated or assurance_details.account_verified is false
|
||||
if !assurance_details.card_holder_authenticated
|
||||
|| !assurance_details.account_verified
|
||||
{
|
||||
logger::info!("Googlepay transaction stepped up to 3DS");
|
||||
self.auth_type = diesel_models::enums::AuthenticationType::ThreeDs;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.auth_type == diesel_models::enums::AuthenticationType::ThreeDs
|
||||
&& !self.request.enrolled_for_3ds
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use router_env::logger;
|
||||
|
||||
use super::{ConstructFlowSpecificData, Feature};
|
||||
use crate::{
|
||||
@ -50,7 +51,7 @@ impl
|
||||
#[async_trait]
|
||||
impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::SetupMandateRouterData {
|
||||
async fn decide_flows<'a>(
|
||||
self,
|
||||
mut self,
|
||||
state: &SessionState,
|
||||
connector: &api::ConnectorData,
|
||||
call_connector_action: payments::CallConnectorAction,
|
||||
@ -62,7 +63,21 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup
|
||||
types::SetupMandateRequestData,
|
||||
types::PaymentsResponseData,
|
||||
> = connector.connector.get_connector_integration();
|
||||
|
||||
// Change the authentication_type to ThreeDs, for google_pay wallet if card_holder_authenticated or account_verified in assurance_details is false
|
||||
if let hyperswitch_domain_models::payment_method_data::PaymentMethodData::Wallet(
|
||||
hyperswitch_domain_models::payment_method_data::WalletData::GooglePay(google_pay_data),
|
||||
) = &self.request.payment_method_data
|
||||
{
|
||||
if let Some(assurance_details) = google_pay_data.info.assurance_details.as_ref() {
|
||||
// Step up the transaction to 3DS when either assurance_details.card_holder_authenticated or assurance_details.account_verified is false
|
||||
if !assurance_details.card_holder_authenticated
|
||||
|| !assurance_details.account_verified
|
||||
{
|
||||
logger::info!("Googlepay transaction stepped up to 3DS");
|
||||
self.auth_type = diesel_models::enums::AuthenticationType::ThreeDs;
|
||||
}
|
||||
}
|
||||
}
|
||||
let resp = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration,
|
||||
|
||||
@ -762,6 +762,13 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
});
|
||||
let (capture_update, mut payment_attempt_update) = match router_data.response.clone() {
|
||||
Err(err) => {
|
||||
let auth_update = if Some(router_data.auth_type)
|
||||
!= payment_data.payment_attempt.authentication_type
|
||||
{
|
||||
Some(router_data.auth_type)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (capture_update, attempt_update) = match payment_data.multiple_capture_data {
|
||||
Some(multiple_capture_data) => {
|
||||
let capture_update = storage::CaptureUpdate::ErrorUpdate {
|
||||
@ -777,7 +784,15 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
multiple_capture_data.get_latest_capture().clone(),
|
||||
capture_update,
|
||||
)];
|
||||
(Some((multiple_capture_data, capture_update_list)), None)
|
||||
(
|
||||
Some((multiple_capture_data, capture_update_list)),
|
||||
auth_update.map(|auth_type| {
|
||||
storage::PaymentAttemptUpdate::AuthenticationTypeUpdate {
|
||||
authentication_type: auth_type,
|
||||
updated_by: storage_scheme.to_string(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
None => {
|
||||
let connector_name = router_data.connector.to_string();
|
||||
@ -835,6 +850,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
unified_message: option_gsm.map(|gsm| gsm.unified_message),
|
||||
connector_transaction_id: err.connector_transaction_id,
|
||||
payment_method_data: additional_payment_method_data,
|
||||
authentication_type: auth_update,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@ -929,6 +945,14 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not parse the connector response")?;
|
||||
|
||||
let auth_update = if Some(router_data.auth_type)
|
||||
!= payment_data.payment_attempt.authentication_type
|
||||
{
|
||||
Some(router_data.auth_type)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// incase of success, update error code and error message
|
||||
let error_status = if router_data.status == enums::AttemptStatus::Charged {
|
||||
Some(None)
|
||||
@ -965,7 +989,15 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
multiple_capture_data.get_latest_capture().clone(),
|
||||
capture_update,
|
||||
)];
|
||||
(Some((multiple_capture_data, capture_update_list)), None)
|
||||
(
|
||||
Some((multiple_capture_data, capture_update_list)),
|
||||
auth_update.map(|auth_type| {
|
||||
storage::PaymentAttemptUpdate::AuthenticationTypeUpdate {
|
||||
authentication_type: auth_type,
|
||||
updated_by: storage_scheme.to_string(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
None => (
|
||||
None,
|
||||
@ -973,7 +1005,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
status: updated_attempt_status,
|
||||
connector: None,
|
||||
connector_transaction_id: connector_transaction_id.clone(),
|
||||
authentication_type: None,
|
||||
authentication_type: auth_update,
|
||||
amount_capturable: router_data
|
||||
.request
|
||||
.get_amount_capturable(&payment_data, updated_attempt_status)
|
||||
|
||||
@ -423,6 +423,13 @@ where
|
||||
}
|
||||
Err(ref error_response) => {
|
||||
let option_gsm = get_gsm(state, &router_data).await?;
|
||||
let auth_update = if Some(router_data.auth_type)
|
||||
!= payment_data.payment_attempt.authentication_type
|
||||
{
|
||||
Some(router_data.auth_type)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
db.update_payment_attempt_with_attempt_id(
|
||||
payment_data.payment_attempt.clone(),
|
||||
@ -438,6 +445,7 @@ where
|
||||
unified_message: option_gsm.map(|gsm| gsm.unified_message),
|
||||
connector_transaction_id: error_response.connector_transaction_id.clone(),
|
||||
payment_method_data: additional_payment_method_data,
|
||||
authentication_type: auth_update,
|
||||
},
|
||||
storage_scheme,
|
||||
)
|
||||
|
||||
@ -134,6 +134,7 @@ impl ProcessTrackerWorkflow<SessionState> for PaymentsSyncWorkflow {
|
||||
unified_message: None,
|
||||
connector_transaction_id: None,
|
||||
payment_method_data: None,
|
||||
authentication_type: None,
|
||||
};
|
||||
|
||||
payment_data.payment_attempt = db
|
||||
|
||||
@ -96,6 +96,7 @@ async fn should_authorize_gpay_payment() {
|
||||
info: domain::GooglePayPaymentMethodInfo {
|
||||
card_network: "VISA".to_string(),
|
||||
card_details: "1234".to_string(),
|
||||
assurance_details: None,
|
||||
},
|
||||
tokenization_data: domain::GpayTokenizationData {
|
||||
token_type: "payu".to_string(),
|
||||
|
||||
@ -69,6 +69,7 @@ async fn should_authorize_gpay_payment() {
|
||||
info: domain::GooglePayPaymentMethodInfo {
|
||||
card_network: "VISA".to_string(),
|
||||
card_details: "1234".to_string(),
|
||||
assurance_details: None,
|
||||
},
|
||||
tokenization_data: domain::GpayTokenizationData {
|
||||
token_type: "worldpay".to_string(),
|
||||
|
||||
@ -1650,6 +1650,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
unified_message,
|
||||
connector_transaction_id,
|
||||
payment_method_data,
|
||||
authentication_type,
|
||||
} => DieselPaymentAttemptUpdate::ErrorUpdate {
|
||||
connector,
|
||||
status,
|
||||
@ -1663,6 +1664,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
unified_message,
|
||||
connector_transaction_id,
|
||||
payment_method_data,
|
||||
authentication_type,
|
||||
},
|
||||
Self::CaptureUpdate {
|
||||
multiple_capture_count,
|
||||
@ -1981,6 +1983,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
unified_message,
|
||||
connector_transaction_id,
|
||||
payment_method_data,
|
||||
authentication_type,
|
||||
} => Self::ErrorUpdate {
|
||||
connector,
|
||||
status,
|
||||
@ -1993,6 +1996,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
unified_message,
|
||||
connector_transaction_id,
|
||||
payment_method_data,
|
||||
authentication_type,
|
||||
},
|
||||
DieselPaymentAttemptUpdate::CaptureUpdate {
|
||||
amount_to_capture,
|
||||
|
||||
Reference in New Issue
Block a user