feat(router): add three_ds_decision_rule support in routing apis (#8132)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sai Harsha Vardhan
2025-05-30 18:21:35 +05:30
committed by GitHub
parent 7a44626251
commit 9bac41b208
19 changed files with 395 additions and 93 deletions

View File

@ -4691,6 +4691,8 @@ impl ProfileWrapper {
storage::enums::TransactionType::Payment => (Some(algorithm_id), None),
#[cfg(feature = "payouts")]
storage::enums::TransactionType::Payout => (None, Some(algorithm_id)),
//TODO: Handle ThreeDsAuthentication Transaction Type for Three DS Decision Rule Algorithm configuration
storage::enums::TransactionType::ThreeDsAuthentication => todo!(),
};
let profile_update = domain::ProfileUpdate::RoutingAlgorithmUpdate {

View File

@ -384,6 +384,8 @@ pub enum RoutingError {
OpenRouterCallFailed,
#[error("Error from open_router: {0}")]
OpenRouterError(String),
#[error("Invalid transaction type")]
InvalidTransactionType,
}
#[derive(Debug, Clone, thiserror::Error)]

View File

@ -518,6 +518,9 @@ async fn ensure_algorithm_cached_v1(
profile_id.get_string_repr()
)
}
common_enums::TransactionType::ThreeDsAuthentication => {
Err(errors::RoutingError::InvalidTransactionType)?
}
}
};
@ -625,6 +628,10 @@ pub async fn refresh_routing_cache_v1(
CachedAlgorithm::Advanced(interpreter)
}
api_models::routing::StaticRoutingAlgorithm::ThreeDsDecisionRule(_program) => {
Err(errors::RoutingError::InvalidRoutingAlgorithmStructure)
.attach_printable("Unsupported algorithm received")?
}
};
let arc_cached_algorithm = Arc::new(cached_algorithm);
@ -722,6 +729,9 @@ pub async fn get_merchant_cgraph(
profile_id.get_string_repr()
)
}
api_enums::TransactionType::ThreeDsAuthentication => {
Err(errors::RoutingError::InvalidTransactionType)?
}
}
};
@ -776,12 +786,18 @@ pub async fn refresh_cgraph_cache(
merchant_connector_accounts
.retain(|mca| mca.connector_type == storage_enums::ConnectorType::PayoutProcessor);
}
api_enums::TransactionType::ThreeDsAuthentication => {
Err(errors::RoutingError::InvalidTransactionType)?
}
};
let connector_type = match transaction_type {
api_enums::TransactionType::Payment => common_enums::ConnectorType::PaymentProcessor,
#[cfg(feature = "payouts")]
api_enums::TransactionType::Payout => common_enums::ConnectorType::PayoutProcessor,
api_enums::TransactionType::ThreeDsAuthentication => {
Err(errors::RoutingError::InvalidTransactionType)?
}
};
let merchant_connector_accounts = merchant_connector_accounts

View File

@ -145,7 +145,7 @@ pub async fn retrieve_merchant_routing_dictionary(
merchant_context: domain::MerchantContext,
profile_id_list: Option<Vec<common_utils::id_type::ProfileId>>,
query_params: RoutingRetrieveQuery,
transaction_type: &enums::TransactionType,
transaction_type: enums::TransactionType,
) -> RouterResponse<routing_types::RoutingKind> {
metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE.add(1, &[]);
@ -153,7 +153,7 @@ pub async fn retrieve_merchant_routing_dictionary(
.store
.list_routing_algorithm_metadata_by_merchant_id_transaction_type(
merchant_context.get_merchant_account().get_id(),
transaction_type,
&transaction_type,
i64::from(query_params.limit.unwrap_or_default()),
i64::from(query_params.offset.unwrap_or_default()),
)
@ -328,14 +328,16 @@ pub async fn create_routing_algorithm_under_profile(
core_utils::validate_profile_id_from_auth_layer(authentication_profile_id, &business_profile)?;
helpers::validate_connectors_in_routing_config(
&state,
merchant_context.get_merchant_key_store(),
merchant_context.get_merchant_account().get_id(),
&profile_id,
&algorithm,
)
.await?;
if algorithm.should_validate_connectors_in_routing_config() {
helpers::validate_connectors_in_routing_config(
&state,
merchant_context.get_merchant_key_store(),
merchant_context.get_merchant_account().get_id(),
&profile_id,
&algorithm,
)
.await?;
}
let mut decision_engine_routing_id: Option<String> = None;
@ -471,7 +473,7 @@ pub async fn link_routing_config(
merchant_context: domain::MerchantContext,
authentication_profile_id: Option<common_utils::id_type::ProfileId>,
algorithm_id: common_utils::id_type::RoutingId,
transaction_type: &enums::TransactionType,
transaction_type: enums::TransactionType,
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
metrics::ROUTING_LINK_CONFIG.add(1, &[]);
let db = state.store.as_ref();
@ -641,7 +643,8 @@ pub async fn link_routing_config(
diesel_models::enums::RoutingAlgorithmKind::Single
| diesel_models::enums::RoutingAlgorithmKind::Priority
| diesel_models::enums::RoutingAlgorithmKind::Advanced
| diesel_models::enums::RoutingAlgorithmKind::VolumeSplit => {
| diesel_models::enums::RoutingAlgorithmKind::VolumeSplit
| diesel_models::enums::RoutingAlgorithmKind::ThreeDsDecisionRule => {
let mut routing_ref: routing_types::RoutingAlgorithmRef = business_profile
.routing_algorithm
.clone()
@ -653,7 +656,7 @@ pub async fn link_routing_config(
)?
.unwrap_or_default();
utils::when(routing_algorithm.algorithm_for != *transaction_type, || {
utils::when(routing_algorithm.algorithm_for != transaction_type, || {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: format!(
"Cannot use {}'s routing algorithm for {} operation",
@ -677,7 +680,7 @@ pub async fn link_routing_config(
merchant_context.get_merchant_key_store(),
business_profile.clone(),
routing_ref,
transaction_type,
&transaction_type,
)
.await?;
}
@ -815,6 +818,8 @@ pub async fn unlink_routing_config_under_profile(
enums::TransactionType::Payment => business_profile.routing_algorithm_id.clone(),
#[cfg(feature = "payouts")]
enums::TransactionType::Payout => business_profile.payout_routing_algorithm_id.clone(),
// TODO: Handle ThreeDsAuthentication Transaction Type for Three DS Decision Rule Algorithm configuration
enums::TransactionType::ThreeDsAuthentication => todo!(),
};
if let Some(algorithm_id) = routing_algo_id {
@ -849,7 +854,7 @@ pub async fn unlink_routing_config(
merchant_context: domain::MerchantContext,
request: routing_types::RoutingConfigRequest,
authentication_profile_id: Option<common_utils::id_type::ProfileId>,
transaction_type: &enums::TransactionType,
transaction_type: enums::TransactionType,
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
metrics::ROUTING_UNLINK_CONFIG.add(1, &[]);
@ -883,6 +888,9 @@ pub async fn unlink_routing_config(
enums::TransactionType::Payment => business_profile.routing_algorithm.clone(),
#[cfg(feature = "payouts")]
enums::TransactionType::Payout => business_profile.payout_routing_algorithm.clone(),
enums::TransactionType::ThreeDsAuthentication => {
business_profile.three_ds_decision_rule_algorithm.clone()
}
}
.map(|val| val.parse_value("RoutingAlgorithmRef"))
.transpose()
@ -916,7 +924,7 @@ pub async fn unlink_routing_config(
merchant_context.get_merchant_key_store(),
business_profile,
routing_algorithm,
transaction_type,
&transaction_type,
)
.await?;
@ -1171,7 +1179,7 @@ pub async fn retrieve_linked_routing_config(
merchant_context: domain::MerchantContext,
authentication_profile_id: Option<common_utils::id_type::ProfileId>,
query_params: routing_types::RoutingRetrieveLinkQuery,
transaction_type: &enums::TransactionType,
transaction_type: enums::TransactionType,
) -> RouterResponse<routing_types::LinkedRoutingConfigRetrieveResponse> {
metrics::ROUTING_RETRIEVE_LINK_CONFIG.add(1, &[]);
@ -1216,6 +1224,9 @@ pub async fn retrieve_linked_routing_config(
enums::TransactionType::Payment => &business_profile.routing_algorithm,
#[cfg(feature = "payouts")]
enums::TransactionType::Payout => &business_profile.payout_routing_algorithm,
enums::TransactionType::ThreeDsAuthentication => {
&business_profile.three_ds_decision_rule_algorithm
}
}
.clone()
.map(|val| val.parse_value("RoutingAlgorithmRef"))

View File

@ -242,25 +242,18 @@ pub async fn update_profile_active_algorithm_ref(
let profile_id = current_business_profile.get_id().to_owned();
let routing_cache_key = cache::CacheKind::Routing(
format!(
"routing_config_{}_{}",
merchant_id.get_string_repr(),
profile_id.get_string_repr(),
)
.into(),
);
let (routing_algorithm, payout_routing_algorithm) = match transaction_type {
storage::enums::TransactionType::Payment => (Some(ref_val), None),
#[cfg(feature = "payouts")]
storage::enums::TransactionType::Payout => (None, Some(ref_val)),
};
let (routing_algorithm, payout_routing_algorithm, three_ds_decision_rule_algorithm) =
match transaction_type {
storage::enums::TransactionType::Payment => (Some(ref_val), None, None),
#[cfg(feature = "payouts")]
storage::enums::TransactionType::Payout => (None, Some(ref_val), None),
storage::enums::TransactionType::ThreeDsAuthentication => (None, None, Some(ref_val)),
};
let business_profile_update = domain::ProfileUpdate::RoutingAlgorithmUpdate {
routing_algorithm,
payout_routing_algorithm,
three_ds_decision_rule_algorithm: None,
three_ds_decision_rule_algorithm,
};
db.update_profile_by_profile_id(
@ -273,10 +266,22 @@ pub async fn update_profile_active_algorithm_ref(
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update routing algorithm ref in business profile")?;
cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [routing_cache_key])
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to invalidate routing cache")?;
// Invalidate the routing cache for Payments and Payouts transaction types
if !transaction_type.is_three_ds_authentication() {
let routing_cache_key = cache::CacheKind::Routing(
format!(
"routing_config_{}_{}",
merchant_id.get_string_repr(),
profile_id.get_string_repr(),
)
.into(),
);
cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [routing_cache_key])
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to invalidate routing cache")?;
}
Ok(())
}
@ -452,6 +457,12 @@ impl RoutingAlgorithmHelpers<'_> {
check_connector_selection(&rule.connector_selection)?;
}
}
routing_types::StaticRoutingAlgorithm::ThreeDsDecisionRule(_) => {
return Err(errors::ApiErrorResponse::InternalServerError).attach_printable(
"Invalid routing algorithm three_ds decision rule received",
)?;
}
}
Ok(())
@ -560,6 +571,11 @@ pub async fn validate_connectors_in_routing_config(
check_connector_selection(&rule.connector_selection)?;
}
}
routing_types::StaticRoutingAlgorithm::ThreeDsDecisionRule(_) => {
Err(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid routing algorithm three_ds decision rule received")?
}
}
Ok(())
@ -581,6 +597,9 @@ pub fn get_default_config_key(
storage::enums::TransactionType::Payment => format!("routing_default_{merchant_id}"),
#[cfg(feature = "payouts")]
storage::enums::TransactionType::Payout => format!("routing_default_po_{merchant_id}"),
storage::enums::TransactionType::ThreeDsAuthentication => {
format!("three_ds_authentication_{merchant_id}")
}
}
}

View File

@ -91,6 +91,7 @@ impl ForeignFrom<storage_enums::RoutingAlgorithmKind> for RoutingAlgorithmKind {
storage_enums::RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit,
storage_enums::RoutingAlgorithmKind::Advanced => Self::Advanced,
storage_enums::RoutingAlgorithmKind::Dynamic => Self::Dynamic,
storage_enums::RoutingAlgorithmKind::ThreeDsDecisionRule => Self::ThreeDsDecisionRule,
}
}
}
@ -103,6 +104,7 @@ impl ForeignFrom<RoutingAlgorithmKind> for storage_enums::RoutingAlgorithmKind {
RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit,
RoutingAlgorithmKind::Advanced => Self::Advanced,
RoutingAlgorithmKind::Dynamic => Self::Dynamic,
RoutingAlgorithmKind::ThreeDsDecisionRule => Self::ThreeDsDecisionRule,
}
}
}

View File

@ -858,43 +858,23 @@ impl Routing {
.app_data(web::Data::new(state.clone()))
.service(
web::resource("/active").route(web::get().to(|state, req, query_params| {
routing::routing_retrieve_linked_config(
state,
req,
query_params,
&TransactionType::Payment,
)
routing::routing_retrieve_linked_config(state, req, query_params, None)
})),
)
.service(
web::resource("")
.route(
web::get().to(|state, req, path: web::Query<RoutingRetrieveQuery>| {
routing::list_routing_configs(
state,
req,
path,
&TransactionType::Payment,
)
routing::list_routing_configs(state, req, path, None)
}),
)
.route(web::post().to(|state, req, payload| {
routing::routing_create_config(
state,
req,
payload,
TransactionType::Payment,
)
routing::routing_create_config(state, req, payload, None)
})),
)
.service(web::resource("/list/profile").route(web::get().to(
|state, req, query: web::Query<RoutingRetrieveQuery>| {
routing::list_routing_configs_for_profile(
state,
req,
query,
&TransactionType::Payment,
)
routing::list_routing_configs_for_profile(state, req, query, None)
},
)))
.service(
@ -909,7 +889,7 @@ impl Routing {
)
.service(
web::resource("/deactivate").route(web::post().to(|state, req, payload| {
routing::routing_unlink_config(state, req, payload, &TransactionType::Payment)
routing::routing_unlink_config(state, req, payload, None)
})),
)
.service(
@ -954,7 +934,7 @@ impl Routing {
state,
req,
path,
&TransactionType::Payout,
Some(TransactionType::Payout),
)
},
))
@ -963,7 +943,7 @@ impl Routing {
state,
req,
payload,
TransactionType::Payout,
Some(TransactionType::Payout),
)
})),
)
@ -973,7 +953,7 @@ impl Routing {
state,
req,
query,
&TransactionType::Payout,
Some(TransactionType::Payout),
)
},
)))
@ -983,7 +963,7 @@ impl Routing {
state,
req,
query_params,
&TransactionType::Payout,
Some(TransactionType::Payout),
)
},
)))
@ -1007,8 +987,14 @@ impl Routing {
)
.service(
web::resource("/payouts/{algorithm_id}/activate").route(web::post().to(
|state, req, path| {
routing::routing_link_config(state, req, path, &TransactionType::Payout)
|state, req, path, payload| {
routing::routing_link_config(
state,
req,
path,
payload,
Some(TransactionType::Payout),
)
},
)),
)
@ -1018,7 +1004,7 @@ impl Routing {
state,
req,
payload,
&TransactionType::Payout,
Some(TransactionType::Payout),
)
},
)))
@ -1053,8 +1039,8 @@ impl Routing {
)
.service(
web::resource("/{algorithm_id}/activate").route(web::post().to(
|state, req, path| {
routing::routing_link_config(state, req, path, &TransactionType::Payment)
|state, req, payload, path| {
routing::routing_link_config(state, req, path, payload, None)
},
)),
);

View File

@ -22,7 +22,7 @@ pub async fn routing_create_config(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<routing_types::RoutingConfigRequest>,
transaction_type: enums::TransactionType,
transaction_type: Option<enums::TransactionType>,
) -> impl Responder {
let flow = Flow::RoutingCreateConfig;
Box::pin(oss_api::server_wrap(
@ -38,8 +38,10 @@ pub async fn routing_create_config(
state,
merchant_context,
auth.profile_id,
payload,
transaction_type,
payload.clone(),
transaction_type
.or(payload.transaction_type)
.unwrap_or(enums::TransactionType::Payment),
)
},
auth::auth_type(
@ -109,7 +111,8 @@ pub async fn routing_link_config(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<common_utils::id_type::RoutingId>,
transaction_type: &enums::TransactionType,
json_payload: web::Json<routing_types::RoutingActivatePayload>,
transaction_type: Option<enums::TransactionType>,
) -> impl Responder {
let flow = Flow::RoutingLinkConfig;
Box::pin(oss_api::server_wrap(
@ -126,7 +129,9 @@ pub async fn routing_link_config(
merchant_context,
auth.profile_id,
algorithm,
transaction_type,
transaction_type
.or(json_payload.transaction_type)
.unwrap_or(enums::TransactionType::Payment),
)
},
auth::auth_type(
@ -289,7 +294,7 @@ pub async fn list_routing_configs(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<RoutingRetrieveQuery>,
transaction_type: &enums::TransactionType,
transaction_type: Option<enums::TransactionType>,
) -> impl Responder {
let flow = Flow::RoutingRetrieveDictionary;
Box::pin(oss_api::server_wrap(
@ -305,8 +310,10 @@ pub async fn list_routing_configs(
state,
merchant_context,
None,
query_params,
transaction_type,
query_params.clone(),
transaction_type
.or(query_params.transaction_type)
.unwrap_or(enums::TransactionType::Payment),
)
},
auth::auth_type(
@ -330,7 +337,7 @@ pub async fn list_routing_configs_for_profile(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<RoutingRetrieveQuery>,
transaction_type: &enums::TransactionType,
transaction_type: Option<enums::TransactionType>,
) -> impl Responder {
let flow = Flow::RoutingRetrieveDictionary;
Box::pin(oss_api::server_wrap(
@ -346,8 +353,10 @@ pub async fn list_routing_configs_for_profile(
state,
merchant_context,
auth.profile_id.map(|profile_id| vec![profile_id]),
query_params,
transaction_type,
query_params.clone(),
transaction_type
.or(query_params.transaction_type)
.unwrap_or(enums::TransactionType::Payment),
)
},
auth::auth_type(
@ -419,7 +428,7 @@ pub async fn routing_unlink_config(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<routing_types::RoutingConfigRequest>,
transaction_type: &enums::TransactionType,
transaction_type: Option<enums::TransactionType>,
) -> impl Responder {
let flow = Flow::RoutingUnlinkConfig;
Box::pin(oss_api::server_wrap(
@ -434,9 +443,11 @@ pub async fn routing_unlink_config(
routing::unlink_routing_config(
state,
merchant_context,
payload_req,
payload_req.clone(),
auth.profile_id,
transaction_type,
transaction_type
.or(payload_req.transaction_type)
.unwrap_or(enums::TransactionType::Payment),
)
},
auth::auth_type(
@ -941,7 +952,7 @@ pub async fn routing_retrieve_linked_config(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<routing_types::RoutingRetrieveLinkQuery>,
transaction_type: &enums::TransactionType,
transaction_type: Option<enums::TransactionType>,
) -> impl Responder {
use crate::services::authentication::AuthenticationData;
let flow = Flow::RoutingRetrieveActiveConfig;
@ -961,7 +972,9 @@ pub async fn routing_retrieve_linked_config(
merchant_context,
auth.profile_id,
query_params,
transaction_type,
transaction_type
.or(query.transaction_type)
.unwrap_or(enums::TransactionType::Payment),
)
},
auth::auth_type(
@ -993,7 +1006,9 @@ pub async fn routing_retrieve_linked_config(
merchant_context,
auth.profile_id,
query_params,
transaction_type,
transaction_type
.or(query.transaction_type)
.unwrap_or(enums::TransactionType::Payment),
)
},
auth::auth_type(