feat: added create endpoint for dynamic_routing (#8755)

Co-authored-by: Ankit Gupta <ankit.gupta.001@Ankit-Gupta-M6QG9L6RHV.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Jagan <jaganelavarasan@gmail.com>
This commit is contained in:
AnkitKmrGupta
2025-08-19 13:08:36 +05:30
committed by GitHub
parent 446590690e
commit 58abb604d7
8 changed files with 410 additions and 10 deletions

View File

@ -2,7 +2,7 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};
use crate::routing::{
ContractBasedRoutingPayloadWrapper, ContractBasedRoutingSetupPayloadWrapper,
DynamicRoutingUpdateConfigQuery, EliminationRoutingPayloadWrapper,
CreateDynamicRoutingWrapper, DynamicRoutingUpdateConfigQuery, EliminationRoutingPayloadWrapper,
LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, ProfileDefaultRoutingConfig,
RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind,
RoutingLinkWrapper, RoutingPayloadWrapper, RoutingRetrieveLinkQuery,
@ -125,6 +125,12 @@ impl ApiEventMetric for ToggleDynamicRoutingWrapper {
}
}
impl ApiEventMetric for CreateDynamicRoutingWrapper {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::Routing)
}
}
impl ApiEventMetric for DynamicRoutingUpdateConfigQuery {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::Routing)

View File

@ -836,6 +836,33 @@ impl DynamicRoutingAlgorithmRef {
};
}
pub fn update_feature(
&mut self,
enabled_feature: DynamicRoutingFeatures,
dynamic_routing_type: DynamicRoutingType,
) {
match dynamic_routing_type {
DynamicRoutingType::SuccessRateBasedRouting => {
self.success_based_algorithm = Some(SuccessBasedAlgorithm {
algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(None),
enabled_feature,
})
}
DynamicRoutingType::EliminationRouting => {
self.elimination_routing_algorithm = Some(EliminationRoutingAlgorithm {
algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(None),
enabled_feature,
})
}
DynamicRoutingType::ContractBasedRouting => {
self.contract_based_routing = Some(ContractRoutingAlgorithm {
algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(None),
enabled_feature,
})
}
};
}
pub fn disable_algorithm_id(&mut self, dynamic_routing_type: DynamicRoutingType) {
match dynamic_routing_type {
DynamicRoutingType::SuccessRateBasedRouting => {
@ -871,6 +898,11 @@ pub struct ToggleDynamicRoutingQuery {
pub enable: DynamicRoutingFeatures,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct CreateDynamicRoutingQuery {
pub enable: DynamicRoutingFeatures,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct DynamicRoutingVolumeSplitQuery {
pub split: u8,
@ -907,6 +939,20 @@ pub struct ToggleDynamicRoutingPath {
pub profile_id: common_utils::id_type::ProfileId,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct CreateDynamicRoutingWrapper {
pub profile_id: common_utils::id_type::ProfileId,
pub feature_to_enable: DynamicRoutingFeatures,
pub payload: DynamicRoutingPayload,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum DynamicRoutingPayload {
SuccessBasedRoutingPayload(SuccessBasedRoutingConfig),
EliminationRoutingPayload(EliminationRoutingConfig),
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct RoutingVolumeSplitResponse {
pub split: u8,

View File

@ -1598,6 +1598,7 @@ pub async fn update_default_routing_config_for_profile(
// Toggle the specific routing type as well as add the default configs in RoutingAlgorithm table
// and update the same in business profile table.
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
pub async fn toggle_specific_dynamic_routing(
state: SessionState,
@ -1647,16 +1648,88 @@ pub async fn toggle_specific_dynamic_routing(
// 1. If present with same feature then return response as already enabled
// 2. Else update the feature and persist the same on db
// 3. If not present in db then create a new default entry
helpers::enable_dynamic_routing_algorithm(
Box::pin(helpers::enable_dynamic_routing_algorithm(
&state,
merchant_context.get_merchant_key_store().clone(),
business_profile,
feature_to_enable,
dynamic_routing_algo_ref,
dynamic_routing_type,
None,
))
.await
}
routing::DynamicRoutingFeatures::None => {
// disable specific dynamic routing for the requested profile
helpers::disable_dynamic_routing_algorithm(
&state,
merchant_context.get_merchant_key_store().clone(),
business_profile,
dynamic_routing_algo_ref,
dynamic_routing_type,
)
.await
}
}
}
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
pub async fn create_specific_dynamic_routing(
state: SessionState,
merchant_context: domain::MerchantContext,
feature_to_enable: routing::DynamicRoutingFeatures,
profile_id: common_utils::id_type::ProfileId,
dynamic_routing_type: routing::DynamicRoutingType,
payload: routing_types::DynamicRoutingPayload,
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(
1,
router_env::metric_attributes!(
("profile_id", profile_id.clone()),
("algorithm_type", dynamic_routing_type.to_string())
),
);
let db = state.store.as_ref();
let key_manager_state = &(&state).into();
let business_profile: domain::Profile = core_utils::validate_and_get_business_profile(
db,
key_manager_state,
merchant_context.get_merchant_key_store(),
Some(&profile_id),
merchant_context.get_merchant_account().get_id(),
)
.await?
.get_required_value("Profile")
.change_context(errors::ApiErrorResponse::ProfileNotFound {
id: profile_id.get_string_repr().to_owned(),
})?;
let dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef = business_profile
.dynamic_routing_algorithm
.clone()
.map(|val| val.parse_value("DynamicRoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to deserialize dynamic routing algorithm ref from business profile",
)?
.unwrap_or_default();
match feature_to_enable {
routing::DynamicRoutingFeatures::Metrics
| routing::DynamicRoutingFeatures::DynamicConnectorSelection => {
Box::pin(helpers::enable_dynamic_routing_algorithm(
&state,
merchant_context.get_merchant_key_store().clone(),
business_profile,
feature_to_enable,
dynamic_routing_algo_ref,
dynamic_routing_type,
Some(payload),
))
.await
}
routing::DynamicRoutingFeatures::None => {
// disable specific dynamic routing for the requested profile
helpers::disable_dynamic_routing_algorithm(

View File

@ -1979,6 +1979,7 @@ pub async fn enable_dynamic_routing_algorithm(
feature_to_enable: routing_types::DynamicRoutingFeatures,
dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
dynamic_routing_type: routing_types::DynamicRoutingType,
payload: Option<routing_types::DynamicRoutingPayload>,
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
let mut dynamic_routing = dynamic_routing_algo_ref.clone();
match dynamic_routing_type {
@ -1994,6 +1995,7 @@ pub async fn enable_dynamic_routing_algorithm(
dynamic_routing.clone(),
dynamic_routing_type,
dynamic_routing.success_based_algorithm,
payload,
)
.await
}
@ -2006,6 +2008,7 @@ pub async fn enable_dynamic_routing_algorithm(
dynamic_routing.clone(),
dynamic_routing_type,
dynamic_routing.elimination_routing_algorithm,
payload,
)
.await
}
@ -2018,6 +2021,7 @@ pub async fn enable_dynamic_routing_algorithm(
}
}
#[allow(clippy::too_many_arguments)]
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
pub async fn enable_specific_routing_algorithm<A>(
state: &SessionState,
@ -2027,10 +2031,24 @@ pub async fn enable_specific_routing_algorithm<A>(
mut dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
dynamic_routing_type: routing_types::DynamicRoutingType,
algo_type: Option<A>,
payload: Option<routing_types::DynamicRoutingPayload>,
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>>
where
A: routing_types::DynamicRoutingAlgoAccessor + Clone + Debug,
{
//Check for payload
if let Some(payload) = payload {
return create_specific_dynamic_routing_setup(
state,
key_store,
business_profile,
feature_to_enable,
dynamic_routing_algo_ref,
dynamic_routing_type,
payload,
)
.await;
}
// Algorithm wasn't created yet
let Some(mut algo_type) = algo_type else {
return default_specific_dynamic_routing_setup(
@ -2112,6 +2130,7 @@ pub async fn default_specific_dynamic_routing_setup(
let merchant_id = business_profile.merchant_id.clone();
let algorithm_id = common_utils::generate_routing_id_of_default_length();
let timestamp = common_utils::date_time::now();
let algo = match dynamic_routing_type {
routing_types::DynamicRoutingType::SuccessRateBasedRouting => {
let default_success_based_routing_config =
@ -2142,6 +2161,7 @@ pub async fn default_specific_dynamic_routing_setup(
} else {
routing_types::EliminationRoutingConfig::default()
};
routing_algorithm::RoutingAlgorithm {
algorithm_id: algorithm_id.clone(),
profile_id: profile_id.clone(),
@ -2173,6 +2193,7 @@ pub async fn default_specific_dynamic_routing_setup(
business_profile.get_id(),
dynamic_routing_type,
&mut dynamic_routing_algo_ref,
None,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
@ -2208,6 +2229,122 @@ pub async fn default_specific_dynamic_routing_setup(
Ok(ApplicationResponse::Json(new_record))
}
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
#[instrument(skip_all)]
pub async fn create_specific_dynamic_routing_setup(
state: &SessionState,
key_store: domain::MerchantKeyStore,
business_profile: domain::Profile,
feature_to_enable: routing_types::DynamicRoutingFeatures,
mut dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
dynamic_routing_type: routing_types::DynamicRoutingType,
payload: routing_types::DynamicRoutingPayload,
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
let db = state.store.as_ref();
let key_manager_state = &state.into();
let profile_id = business_profile.get_id().clone();
let merchant_id = business_profile.merchant_id.clone();
let algorithm_id = common_utils::generate_routing_id_of_default_length();
let timestamp = common_utils::date_time::now();
let algo = match dynamic_routing_type {
routing_types::DynamicRoutingType::SuccessRateBasedRouting => {
let success_config = match &payload {
routing_types::DynamicRoutingPayload::SuccessBasedRoutingPayload(config) => config,
_ => {
return Err((errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid payload type for Success Rate Based Routing".to_string(),
})
.into())
}
};
routing_algorithm::RoutingAlgorithm {
algorithm_id: algorithm_id.clone(),
profile_id: profile_id.clone(),
merchant_id,
name: SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM.to_string(),
description: None,
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
algorithm_data: serde_json::json!(success_config),
created_at: timestamp,
modified_at: timestamp,
algorithm_for: common_enums::TransactionType::Payment,
decision_engine_routing_id: None,
}
}
routing_types::DynamicRoutingType::EliminationRouting => {
let elimination_config = match &payload {
routing_types::DynamicRoutingPayload::EliminationRoutingPayload(config) => config,
_ => {
return Err((errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid payload type for Elimination Routing".to_string(),
})
.into())
}
};
routing_algorithm::RoutingAlgorithm {
algorithm_id: algorithm_id.clone(),
profile_id: profile_id.clone(),
merchant_id,
name: ELIMINATION_BASED_DYNAMIC_ROUTING_ALGORITHM.to_string(),
description: None,
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
algorithm_data: serde_json::json!(elimination_config),
created_at: timestamp,
modified_at: timestamp,
algorithm_for: common_enums::TransactionType::Payment,
decision_engine_routing_id: None,
}
}
routing_types::DynamicRoutingType::ContractBasedRouting => {
return Err((errors::ApiErrorResponse::InvalidRequestData {
message: "Contract routing cannot be set as default".to_string(),
})
.into())
}
};
if state.conf.open_router.dynamic_routing_enabled {
enable_decision_engine_dynamic_routing_setup(
state,
business_profile.get_id(),
dynamic_routing_type,
&mut dynamic_routing_algo_ref,
Some(payload),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to setup decision engine dynamic routing")?;
}
let record = db
.insert_routing_algorithm(algo)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to insert record in routing algorithm table")?;
dynamic_routing_algo_ref.update_feature(feature_to_enable, dynamic_routing_type);
update_business_profile_active_dynamic_algorithm_ref(
db,
key_manager_state,
&key_store,
business_profile,
dynamic_routing_algo_ref,
)
.await?;
let new_record = record.foreign_into();
core_metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
1,
router_env::metric_attributes!(("profile_id", profile_id.clone())),
);
Ok(ApplicationResponse::Json(new_record))
}
#[derive(Debug, Clone)]
pub struct DynamicRoutingConfigParamsInterpolator {
pub payment_method: Option<common_enums::PaymentMethod>,
@ -2289,20 +2426,30 @@ pub async fn enable_decision_engine_dynamic_routing_setup(
profile_id: &id_type::ProfileId,
dynamic_routing_type: routing_types::DynamicRoutingType,
dynamic_routing_algo_ref: &mut routing_types::DynamicRoutingAlgorithmRef,
payload: Option<routing_types::DynamicRoutingPayload>,
) -> RouterResult<()> {
logger::debug!(
"performing call with open_router for profile {}",
profile_id.get_string_repr()
);
let default_engine_config_request = match dynamic_routing_type {
let decision_engine_config_request = match dynamic_routing_type {
routing_types::DynamicRoutingType::SuccessRateBasedRouting => {
let default_success_based_routing_config =
routing_types::SuccessBasedRoutingConfig::open_router_config_default();
let success_based_routing_config = payload
.and_then(|p| match p {
routing_types::DynamicRoutingPayload::SuccessBasedRoutingPayload(config) => {
Some(config)
}
_ => None,
})
.unwrap_or_else(
routing_types::SuccessBasedRoutingConfig::open_router_config_default,
);
open_router::DecisionEngineConfigSetupRequest {
merchant_id: profile_id.get_string_repr().to_string(),
config: open_router::DecisionEngineConfigVariant::SuccessRate(
default_success_based_routing_config
success_based_routing_config
.get_decision_engine_configs()
.change_context(errors::ApiErrorResponse::GenericNotFoundError {
message: "Decision engine config not found".to_string(),
@ -2312,12 +2459,21 @@ pub async fn enable_decision_engine_dynamic_routing_setup(
}
}
routing_types::DynamicRoutingType::EliminationRouting => {
let default_elimination_based_routing_config =
routing_types::EliminationRoutingConfig::open_router_config_default();
let elimination_based_routing_config = payload
.and_then(|p| match p {
routing_types::DynamicRoutingPayload::EliminationRoutingPayload(config) => {
Some(config)
}
_ => None,
})
.unwrap_or_else(
routing_types::EliminationRoutingConfig::open_router_config_default,
);
open_router::DecisionEngineConfigSetupRequest {
merchant_id: profile_id.get_string_repr().to_string(),
config: open_router::DecisionEngineConfigVariant::Elimination(
default_elimination_based_routing_config
elimination_based_routing_config
.get_decision_engine_configs()
.change_context(errors::ApiErrorResponse::GenericNotFoundError {
message: "Decision engine config not found".to_string(),
@ -2342,7 +2498,7 @@ pub async fn enable_decision_engine_dynamic_routing_setup(
state,
services::Method::Post,
DECISION_ENGINE_RULE_CREATE_ENDPOINT,
Some(default_engine_config_request),
Some(decision_engine_config_request),
None,
None,
)

View File

@ -2193,6 +2193,10 @@ impl Profile {
web::resource("/toggle")
.route(web::post().to(routing::toggle_success_based_routing)),
)
.service(
web::resource("/create")
.route(web::post().to(routing::create_success_based_routing)),
)
.service(web::resource("/config/{algorithm_id}").route(
web::patch().to(|state, req, path, payload| {
routing::success_based_routing_update_configs(
@ -2215,6 +2219,10 @@ impl Profile {
web::resource("/toggle")
.route(web::post().to(routing::toggle_elimination_routing)),
)
.service(
web::resource("/create")
.route(web::post().to(routing::create_elimination_routing)),
)
.service(web::resource("config/{algorithm_id}").route(
web::patch().to(|state, req, path, payload| {
routing::elimination_routing_update_configs(

View File

@ -79,6 +79,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::DecisionManagerDeleteConfig
| Flow::DecisionManagerRetrieveConfig
| Flow::ToggleDynamicRouting
| Flow::CreateDynamicRoutingConfig
| Flow::UpdateDynamicRoutingConfigs
| Flow::DecisionManagerUpsertConfig
| Flow::RoutingEvaluateRule

View File

@ -1240,6 +1240,60 @@ pub async fn toggle_success_based_routing(
.await
}
#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))]
#[instrument(skip_all)]
pub async fn create_success_based_routing(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<api_models::routing::CreateDynamicRoutingQuery>,
path: web::Path<routing_types::ToggleDynamicRoutingPath>,
success_based_config: web::Json<routing_types::SuccessBasedRoutingConfig>,
) -> impl Responder {
let flow = Flow::CreateDynamicRoutingConfig;
let wrapper = routing_types::CreateDynamicRoutingWrapper {
feature_to_enable: query.into_inner().enable,
profile_id: path.into_inner().profile_id,
payload: api_models::routing::DynamicRoutingPayload::SuccessBasedRoutingPayload(
success_based_config.into_inner(),
),
};
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
wrapper.clone(),
|state,
auth: auth::AuthenticationData,
wrapper: routing_types::CreateDynamicRoutingWrapper,
_| {
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store),
));
routing::create_specific_dynamic_routing(
state,
merchant_context,
wrapper.feature_to_enable,
wrapper.profile_id,
api_models::routing::DynamicRoutingType::SuccessRateBasedRouting,
wrapper.payload,
)
},
auth::auth_type(
&auth::HeaderAuth(auth::ApiKeyAuth {
is_connected_allowed: false,
is_platform_allowed: false,
}),
&auth::JWTAuthProfileFromRoute {
profile_id: wrapper.profile_id,
required_permission: Permission::ProfileRoutingWrite,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))]
#[instrument(skip_all)]
pub async fn success_based_routing_update_configs(
@ -1480,6 +1534,60 @@ pub async fn toggle_elimination_routing(
.await
}
#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))]
#[instrument(skip_all)]
pub async fn create_elimination_routing(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<api_models::routing::CreateDynamicRoutingQuery>,
path: web::Path<routing_types::ToggleDynamicRoutingPath>,
elimination_config: web::Json<routing_types::EliminationRoutingConfig>,
) -> impl Responder {
let flow = Flow::CreateDynamicRoutingConfig;
let wrapper = routing_types::CreateDynamicRoutingWrapper {
feature_to_enable: query.into_inner().enable,
profile_id: path.into_inner().profile_id,
payload: api_models::routing::DynamicRoutingPayload::EliminationRoutingPayload(
elimination_config.into_inner(),
),
};
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
wrapper.clone(),
|state,
auth: auth::AuthenticationData,
wrapper: routing_types::CreateDynamicRoutingWrapper,
_| {
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store),
));
routing::create_specific_dynamic_routing(
state,
merchant_context,
wrapper.feature_to_enable,
wrapper.profile_id,
api_models::routing::DynamicRoutingType::EliminationRouting,
wrapper.payload,
)
},
auth::auth_type(
&auth::HeaderAuth(auth::ApiKeyAuth {
is_connected_allowed: false,
is_platform_allowed: false,
}),
&auth::JWTAuthProfileFromRoute {
profile_id: wrapper.profile_id,
required_permission: Permission::ProfileRoutingWrite,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(all(feature = "olap", feature = "v1"))]
#[instrument(skip_all)]
pub async fn set_dynamic_routing_volume_split(

View File

@ -261,6 +261,8 @@ pub enum Flow {
RoutingUpdateDefaultConfig,
/// Routing delete config
RoutingDeleteConfig,
/// Create dynamic routing
CreateDynamicRoutingConfig,
/// Toggle dynamic routing
ToggleDynamicRouting,
/// Update dynamic routing config