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:
Shanks
2024-06-24 19:39:50 +05:30
committed by GitHub
parent b7bf457d0c
commit ff84d78c65
16 changed files with 149 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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