Skip to content

Commit

Permalink
feat(core): 3ds decision manager for v2 (#7089)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
swangi-kumari and hyperswitch-bot[bot] authored Feb 12, 2025
1 parent fa09db1 commit 52ae92b
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 26 deletions.
17 changes: 14 additions & 3 deletions api-reference-v2/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -15042,7 +15042,8 @@
"created",
"payment_method_type",
"payment_method_subtype",
"merchant_connector_id"
"merchant_connector_id",
"applied_authentication_type"
],
"properties": {
"id": {
Expand Down Expand Up @@ -15141,6 +15142,17 @@
}
],
"nullable": true
},
"authentication_type": {
"allOf": [
{
"$ref": "#/components/schemas/AuthenticationType"
}
],
"nullable": true
},
"applied_authentication_type": {
"$ref": "#/components/schemas/AuthenticationType"
}
}
},
Expand Down Expand Up @@ -15499,7 +15511,6 @@
"client_secret",
"profile_id",
"capture_method",
"authentication_type",
"customer_id",
"customer_present",
"setup_future_usage",
Expand Down Expand Up @@ -15551,7 +15562,7 @@
"$ref": "#/components/schemas/AuthenticationType"
}
],
"default": "no_three_ds"
"nullable": true
},
"billing": {
"allOf": [
Expand Down
13 changes: 11 additions & 2 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,9 @@ pub struct PaymentsIntentResponse {
#[schema(value_type = CaptureMethod, example = "automatic")]
pub capture_method: api_enums::CaptureMethod,

#[schema(value_type = AuthenticationType, example = "no_three_ds", default = "no_three_ds")]
pub authentication_type: api_enums::AuthenticationType,
/// The authentication type for the payment
#[schema(value_type = Option<AuthenticationType>, example = "no_three_ds")]
pub authentication_type: Option<api_enums::AuthenticationType>,

/// The billing details of the payment. This address will be used for invoicing.
#[schema(value_type = Option<Address>)]
Expand Down Expand Up @@ -5270,6 +5271,14 @@ pub struct PaymentsConfirmIntentResponse {

/// Error details for the payment if any
pub error: Option<ErrorDetails>,

/// The transaction authentication can be set to undergo payer authentication. By default, the authentication will be marked as NO_THREE_DS
#[schema(value_type = Option<AuthenticationType>, example = "no_three_ds")]
pub authentication_type: Option<api_enums::AuthenticationType>,

/// The authentication type applied for the payment
#[schema(value_type = AuthenticationType, example = "no_three_ds")]
pub applied_authentication_type: api_enums::AuthenticationType,
}

/// Token information that can be used to initiate transactions by the merchant.
Expand Down
2 changes: 1 addition & 1 deletion crates/diesel_models/src/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ pub enum PaymentAttemptUpdate {
#[diesel(table_name = payment_attempt)]
pub struct PaymentAttemptUpdateInternal {
pub status: Option<storage_enums::AttemptStatus>,
// authentication_type: Option<storage_enums::AuthenticationType>,
pub authentication_type: Option<storage_enums::AuthenticationType>,
pub error_message: Option<String>,
pub connector_payment_id: Option<String>,
// payment_method_id: Option<String>,
Expand Down
4 changes: 2 additions & 2 deletions crates/hyperswitch_domain_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ pub struct PaymentIntent {
/// Capture method for the payment
pub capture_method: storage_enums::CaptureMethod,
/// Authentication type that is requested by the merchant for this payment.
pub authentication_type: common_enums::AuthenticationType,
pub authentication_type: Option<common_enums::AuthenticationType>,
/// This contains the pre routing results that are done when routing is done during listing the payment methods.
pub prerouting_algorithm: Option<Value>,
/// The organization id for the payment. This is derived from the merchant account
Expand Down Expand Up @@ -498,7 +498,7 @@ impl PaymentIntent {
.change_context(errors::api_error_response::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to decode shipping address")?,
capture_method: request.capture_method.unwrap_or_default(),
authentication_type: request.authentication_type.unwrap_or_default(),
authentication_type: request.authentication_type,
prerouting_algorithm: None,
organization_id: merchant_account.organization_id.clone(),
enable_payment_link: request.payment_link_enabled.unwrap_or_default(),
Expand Down
11 changes: 10 additions & 1 deletion crates/hyperswitch_domain_models/src/payments/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ impl PaymentAttempt {
.transpose()
.change_context(errors::api_error_response::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to decode billing address")?;
let authentication_type = payment_intent.authentication_type.unwrap_or_default();

Ok(Self {
payment_id: payment_intent.id.clone(),
Expand All @@ -498,7 +499,7 @@ impl PaymentAttempt {
// This will be decided by the routing algorithm and updated in update trackers
// right before calling the connector
connector: None,
authentication_type: payment_intent.authentication_type,
authentication_type,
created_at: now,
modified_at: now,
last_synced: None,
Expand Down Expand Up @@ -1453,6 +1454,7 @@ pub enum PaymentAttemptUpdate {
updated_by: String,
connector: String,
merchant_connector_id: id_type::MerchantConnectorAccountId,
authentication_type: storage_enums::AuthenticationType,
},
/// Update the payment attempt on confirming the intent, after calling the connector on success response
ConfirmIntentResponse(Box<ConfirmIntentResponseUpdate>),
Expand Down Expand Up @@ -2108,6 +2110,7 @@ impl From<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
updated_by,
connector,
merchant_connector_id,
authentication_type,
} => Self {
status: Some(status),
error_message: None,
Expand All @@ -2126,6 +2129,7 @@ impl From<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
amount_capturable: None,
amount_to_capture: None,
connector_token_details: None,
authentication_type: Some(authentication_type),
},
PaymentAttemptUpdate::ErrorUpdate {
status,
Expand All @@ -2151,6 +2155,7 @@ impl From<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
amount_capturable,
amount_to_capture: None,
connector_token_details: None,
authentication_type: None,
},
PaymentAttemptUpdate::ConfirmIntentResponse(confirm_intent_response_update) => {
let ConfirmIntentResponseUpdate {
Expand Down Expand Up @@ -2181,6 +2186,7 @@ impl From<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
connector_metadata,
amount_to_capture: None,
connector_token_details,
authentication_type: None,
}
}
PaymentAttemptUpdate::SyncUpdate {
Expand All @@ -2205,6 +2211,7 @@ impl From<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
connector_metadata: None,
amount_to_capture: None,
connector_token_details: None,
authentication_type: None,
},
PaymentAttemptUpdate::CaptureUpdate {
status,
Expand All @@ -2228,6 +2235,7 @@ impl From<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
redirection_data: None,
connector_metadata: None,
connector_token_details: None,
authentication_type: None,
},
PaymentAttemptUpdate::PreCaptureUpdate {
amount_to_capture,
Expand All @@ -2250,6 +2258,7 @@ impl From<PaymentAttemptUpdate> for diesel_models::PaymentAttemptUpdateInternal
connector_metadata: None,
amount_capturable: None,
connector_token_details: None,
authentication_type: None,
},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1387,7 +1387,7 @@ impl behaviour::Conversion for PaymentIntent {
shipping_address: shipping_address.map(Encryption::from),
capture_method: Some(capture_method),
id,
authentication_type: Some(authentication_type),
authentication_type,
prerouting_algorithm,
merchant_reference_id,
surcharge_amount: amount_details.surcharge_amount,
Expand Down Expand Up @@ -1521,7 +1521,7 @@ impl behaviour::Conversion for PaymentIntent {
id: storage_model.id,
merchant_reference_id: storage_model.merchant_reference_id,
organization_id: storage_model.organization_id,
authentication_type: storage_model.authentication_type.unwrap_or_default(),
authentication_type: storage_model.authentication_type,
prerouting_algorithm: storage_model.prerouting_algorithm,
enable_payment_link: storage_model.enable_payment_link.into(),
apply_mit_exemption: storage_model.apply_mit_exemption.into(),
Expand Down Expand Up @@ -1592,7 +1592,7 @@ impl behaviour::Conversion for PaymentIntent {
capture_method: Some(self.capture_method),
id: self.id,
merchant_reference_id: self.merchant_reference_id,
authentication_type: Some(self.authentication_type),
authentication_type: self.authentication_type,
prerouting_algorithm: self.prerouting_algorithm,
surcharge_amount: amount_details.surcharge_amount,
tax_on_surcharge: amount_details.tax_on_surcharge,
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/core/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2025,7 +2025,7 @@ impl DefaultFallbackRoutingConfigUpdate<'_> {
};
if default_routing_config_for_profile.contains(&choice.clone()) {
default_routing_config_for_profile.retain(|mca| {
(mca.merchant_connector_id.as_ref() != Some(self.merchant_connector_id))
mca.merchant_connector_id.as_ref() != Some(self.merchant_connector_id)
});

profile_wrapper
Expand Down
32 changes: 26 additions & 6 deletions crates/router/src/core/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ where
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)
.attach_printable("Failed while fetching/creating customer")?;

operation
.to_domain()?
.run_decision_manager(state, &mut payment_data, &profile)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to run decision manager")?;

let connector = operation
.to_domain()?
.perform_routing(
Expand Down Expand Up @@ -1152,17 +1159,30 @@ where
// TODO: Move to business profile surcharge column
#[instrument(skip_all)]
#[cfg(feature = "v2")]
pub async fn call_decision_manager<F, D>(
pub fn call_decision_manager<F>(
state: &SessionState,
merchant_account: &domain::MerchantAccount,
_business_profile: &domain::Profile,
payment_data: &D,
record: common_types::payments::DecisionManagerRecord,
payment_data: &PaymentConfirmData<F>,
) -> RouterResult<Option<enums::AuthenticationType>>
where
F: Clone,
D: OperationSessionGetters<F>,
{
todo!()
let payment_method_data = payment_data.get_payment_method_data();
let payment_dsl_data = core_routing::PaymentsDslInput::new(
None,
payment_data.get_payment_attempt(),
payment_data.get_payment_intent(),
payment_method_data,
payment_data.get_address(),
None,
payment_data.get_currency(),
);

let output = perform_decision_management(record, &payment_dsl_data)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the conditional config")?;

Ok(output.override_3ds)
}

#[cfg(feature = "v2")]
Expand Down
20 changes: 20 additions & 0 deletions crates/router/src/core/payments/conditional_configs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ use router_env::{instrument, tracing};
use storage_impl::redis::cache::{self, DECISION_MANAGER_CACHE};

use super::routing::make_dsl_input;
#[cfg(feature = "v2")]
use crate::{core::errors::RouterResult, types::domain};
use crate::{
core::{errors, errors::ConditionalConfigError as ConfigError, routing as core_routing},
routes,
};
pub type ConditionalConfigResult<O> = errors::CustomResult<O, ConfigError>;

#[instrument(skip_all)]
#[cfg(feature = "v1")]
pub async fn perform_decision_management(
state: &routes::SessionState,
algorithm_ref: routing::RoutingAlgorithmRef,
Expand Down Expand Up @@ -57,6 +60,23 @@ pub async fn perform_decision_management(
execute_dsl_and_get_conditional_config(backend_input, &interpreter)
}

#[cfg(feature = "v2")]
pub fn perform_decision_management(
record: common_types::payments::DecisionManagerRecord,
payment_data: &core_routing::PaymentsDslInput<'_>,
) -> RouterResult<common_types::payments::ConditionalConfigs> {
let interpreter = backend::VirInterpreterBackend::with_program(record.program)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error initializing DSL interpreter backend")?;

let backend_input = make_dsl_input(payment_data)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error constructing DSL input")?;
execute_dsl_and_get_conditional_config(backend_input, &interpreter)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error executing DSL")
}

pub fn execute_dsl_and_get_conditional_config(
backend_input: dsl_inputs::BackendInput,
interpreter: &backend::VirInterpreterBackend<common_types::payments::ConditionalConfigs>,
Expand Down
11 changes: 11 additions & 0 deletions crates/router/src/core/payments/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,17 @@ pub trait Domain<F: Clone, R, D>: Send + Sync {
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<(BoxedOperation<'a, F, R, D>, Option<domain::Customer>), errors::StorageError>;

#[cfg(feature = "v2")]
/// This will run the decision manager for the payment
async fn run_decision_manager<'a>(
&'a self,
state: &SessionState,
payment_data: &mut D,
business_profile: &domain::Profile,
) -> CustomResult<(), errors::ApiErrorResponse> {
Ok(())
}

#[allow(clippy::too_many_arguments)]
async fn make_pm_data<'a>(
&'a self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ use crate::{
admin,
errors::{self, CustomResult, RouterResult, StorageErrorExt},
payments::{
self, helpers,
self, call_decision_manager, helpers,
operations::{self, ValidateStatusForOperation},
populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData,
populate_surcharge_details, CustomerDetails, OperationSessionSetters, PaymentAddress,
PaymentData,
},
utils as core_utils,
},
Expand Down Expand Up @@ -290,6 +291,30 @@ impl<F: Clone + Send + Sync> Domain<F, PaymentsConfirmIntentRequest, PaymentConf
}
}

async fn run_decision_manager<'a>(
&'a self,
state: &SessionState,
payment_data: &mut PaymentConfirmData<F>,
business_profile: &domain::Profile,
) -> CustomResult<(), errors::ApiErrorResponse> {
let authentication_type = payment_data.payment_intent.authentication_type;

let authentication_type = match business_profile.three_ds_decision_manager_config.as_ref() {
Some(three_ds_decision_manager_config) => call_decision_manager(
state,
three_ds_decision_manager_config.clone(),
payment_data,
)?,
None => authentication_type,
};

if let Some(auth_type) = authentication_type {
payment_data.payment_attempt.authentication_type = auth_type;
}

Ok(())
}

#[instrument(skip_all)]
async fn make_pm_data<'a>(
&'a self,
Expand Down Expand Up @@ -397,11 +422,14 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentConfirmData<F>, PaymentsConfirmInt
active_attempt_id: payment_data.payment_attempt.id.clone(),
};

let authentication_type = payment_data.payment_attempt.authentication_type;

let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntent {
status: attempt_status,
updated_by: storage_scheme.to_string(),
connector,
merchant_connector_id,
authentication_type,
};

let updated_payment_intent = db
Expand Down
Loading

0 comments on commit 52ae92b

Please sign in to comment.