feat(core): 3ds decision manager for v2 (#7089)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Swangi Kumari
2025-02-12 18:11:29 +05:30
committed by GitHub
parent fa09db1534
commit 52ae92bc5d
14 changed files with 244 additions and 26 deletions

View File

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

View File

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

View File

@ -6,6 +6,8 @@ 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,
@ -13,6 +15,7 @@ use crate::{
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,
@ -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>,

View File

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

View File

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

View File

@ -259,7 +259,7 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentIntentData<F>, PaymentsUpda
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to decode shipping address")?,
capture_method: capture_method.unwrap_or(payment_intent.capture_method),
authentication_type: authentication_type.unwrap_or(payment_intent.authentication_type),
authentication_type: authentication_type.or(payment_intent.authentication_type),
payment_link_config: payment_link_config
.map(ApiModelToDieselModelConvertor::convert_from)
.or(payment_intent.payment_link_config),
@ -324,7 +324,7 @@ impl<F: Clone> UpdateTracker<F, payments::PaymentIntentData<F>, PaymentsUpdateIn
tax_on_surcharge: intent.amount_details.tax_on_surcharge,
routing_algorithm_id: intent.routing_algorithm_id,
capture_method: Some(intent.capture_method),
authentication_type: Some(intent.authentication_type),
authentication_type: intent.authentication_type,
billing_address: intent.billing_address,
shipping_address: intent.shipping_address,
customer_present: Some(intent.customer_present),

View File

@ -173,7 +173,112 @@ pub fn make_dsl_input_for_payouts(
pub fn make_dsl_input(
payments_dsl_input: &routing::PaymentsDslInput<'_>,
) -> RoutingResult<dsl_inputs::BackendInput> {
todo!()
let mandate_data = dsl_inputs::MandateData {
mandate_acceptance_type: payments_dsl_input.setup_mandate.as_ref().and_then(
|mandate_data| {
mandate_data
.customer_acceptance
.as_ref()
.map(|customer_accept| match customer_accept.acceptance_type {
hyperswitch_domain_models::mandates::AcceptanceType::Online => {
euclid_enums::MandateAcceptanceType::Online
}
hyperswitch_domain_models::mandates::AcceptanceType::Offline => {
euclid_enums::MandateAcceptanceType::Offline
}
})
},
),
mandate_type: payments_dsl_input
.setup_mandate
.as_ref()
.and_then(|mandate_data| {
mandate_data
.mandate_type
.clone()
.map(|mandate_type| match mandate_type {
hyperswitch_domain_models::mandates::MandateDataType::SingleUse(_) => {
euclid_enums::MandateType::SingleUse
}
hyperswitch_domain_models::mandates::MandateDataType::MultiUse(_) => {
euclid_enums::MandateType::MultiUse
}
})
}),
payment_type: Some(
if payments_dsl_input
.recurring_details
.as_ref()
.is_some_and(|data| {
matches!(
data,
api_models::mandates::RecurringDetails::ProcessorPaymentToken(_)
)
})
{
euclid_enums::PaymentType::PptMandate
} else {
payments_dsl_input.setup_mandate.map_or_else(
|| euclid_enums::PaymentType::NonMandate,
|_| euclid_enums::PaymentType::SetupMandate,
)
},
),
};
let payment_method_input = dsl_inputs::PaymentMethodInput {
payment_method: Some(payments_dsl_input.payment_attempt.payment_method_type),
payment_method_type: Some(payments_dsl_input.payment_attempt.payment_method_subtype),
card_network: payments_dsl_input
.payment_method_data
.as_ref()
.and_then(|pm_data| match pm_data {
domain::PaymentMethodData::Card(card) => card.card_network.clone(),
_ => None,
}),
};
let payment_input = dsl_inputs::PaymentInput {
amount: payments_dsl_input
.payment_attempt
.amount_details
.get_net_amount(),
card_bin: payments_dsl_input.payment_method_data.as_ref().and_then(
|pm_data| match pm_data {
domain::PaymentMethodData::Card(card) => Some(card.card_number.get_card_isin()),
_ => None,
},
),
currency: payments_dsl_input.currency,
authentication_type: Some(payments_dsl_input.payment_attempt.authentication_type),
capture_method: Some(payments_dsl_input.payment_intent.capture_method),
business_country: None,
billing_country: payments_dsl_input
.address
.get_payment_method_billing()
.and_then(|billing_address| billing_address.address.as_ref())
.and_then(|address_details| address_details.country)
.map(api_enums::Country::from_alpha2),
business_label: None,
setup_future_usage: Some(payments_dsl_input.payment_intent.setup_future_usage),
};
let metadata = payments_dsl_input
.payment_intent
.metadata
.clone()
.map(|value| value.parse_value("routing_parameters"))
.transpose()
.change_context(errors::RoutingError::MetadataParsingError)
.attach_printable("Unable to parse routing_parameters from metadata of payment_intent")
.unwrap_or(None);
Ok(dsl_inputs::BackendInput {
metadata,
payment: payment_input,
payment_method: payment_method_input,
mandate: mandate_data,
})
}
#[cfg(feature = "v1")]
@ -185,7 +290,7 @@ pub fn make_dsl_input(
|mandate_data| {
mandate_data
.customer_acceptance
.clone()
.as_ref()
.map(|cat| match cat.acceptance_type {
hyperswitch_domain_models::mandates::AcceptanceType::Online => {
euclid_enums::MandateAcceptanceType::Online

View File

@ -787,7 +787,10 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>(
.map(ToOwned::to_owned),
// TODO: Create unified address
address: hyperswitch_domain_models::payment_address::PaymentAddress::default(),
auth_type: payment_data.payment_intent.authentication_type,
auth_type: payment_data
.payment_intent
.authentication_type
.unwrap_or_default(),
connector_meta_data: merchant_connector_account.get_metadata(),
connector_wallets_details: None,
request,
@ -1647,6 +1650,8 @@ where
merchant_connector_id,
browser_info: None,
error,
authentication_type: payment_intent.authentication_type,
applied_authentication_type: payment_attempt.authentication_type,
};
Ok(services::ApplicationResponse::JsonWithHeaders((