mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 21:07:58 +08:00
feat(euclid): add dynamic routing in core flows (#6333)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -328,6 +328,20 @@ pub enum RoutingError {
|
||||
VolumeSplitFailed,
|
||||
#[error("Unable to parse metadata")]
|
||||
MetadataParsingError,
|
||||
#[error("Unable to retrieve success based routing config")]
|
||||
SuccessBasedRoutingConfigError,
|
||||
#[error("Unable to calculate success based routing config from dynamic routing service")]
|
||||
SuccessRateCalculationError,
|
||||
#[error("Success rate client from dynamic routing gRPC service not initialized")]
|
||||
SuccessRateClientInitializationError,
|
||||
#[error("Unable to convert from '{from}' to '{to}'")]
|
||||
GenericConversionError { from: String, to: String },
|
||||
#[error("Invalid success based connector label received from dynamic routing service: '{0}'")]
|
||||
InvalidSuccessBasedConnectorLabel(String),
|
||||
#[error("unable to find '{field}'")]
|
||||
GenericNotFoundError { field: String },
|
||||
#[error("Unable to deserialize from '{from}' to '{to}'")]
|
||||
DeserializationError { from: String, to: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
|
||||
@ -5583,6 +5583,19 @@ where
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("failed eligibility analysis and fallback")?;
|
||||
|
||||
// dynamic success based connector selection
|
||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||
let connectors = {
|
||||
if business_profile.dynamic_routing_algorithm.is_some() {
|
||||
routing::perform_success_based_routing(state, connectors.clone(), business_profile)
|
||||
.await
|
||||
.map_err(|e| logger::error!(success_rate_routing_error=?e))
|
||||
.unwrap_or(connectors)
|
||||
} else {
|
||||
connectors
|
||||
}
|
||||
};
|
||||
|
||||
let connector_data = connectors
|
||||
.into_iter()
|
||||
.map(|conn| {
|
||||
|
||||
@ -1918,8 +1918,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
|
||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||
{
|
||||
if let Some(dynamic_routing_algorithm) = business_profile.dynamic_routing_algorithm.clone()
|
||||
{
|
||||
if business_profile.dynamic_routing_algorithm.is_some() {
|
||||
let state = state.clone();
|
||||
let business_profile = business_profile.clone();
|
||||
let payment_attempt = payment_attempt.clone();
|
||||
@ -1930,7 +1929,6 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
||||
&payment_attempt,
|
||||
routable_connectors,
|
||||
&business_profile,
|
||||
dynamic_routing_algorithm,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| logger::error!(dynamic_routing_metrics_error=?e))
|
||||
|
||||
@ -7,6 +7,8 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||
use api_models::routing as api_routing;
|
||||
use api_models::{
|
||||
admin as admin_api,
|
||||
enums::{self as api_enums, CountryAlpha2},
|
||||
@ -21,6 +23,10 @@ use euclid::{
|
||||
enums as euclid_enums,
|
||||
frontend::{ast, dir as euclid_dir},
|
||||
};
|
||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||
use external_services::grpc_client::dynamic_routing::{
|
||||
success_rate::CalSuccessRateResponse, SuccessBasedDynamicRouting,
|
||||
};
|
||||
use kgraph_utils::{
|
||||
mca as mca_graph,
|
||||
transformers::{IntoContext, IntoDirValue},
|
||||
@ -1227,3 +1233,114 @@ pub fn make_dsl_input_for_surcharge(
|
||||
};
|
||||
Ok(backend_input)
|
||||
}
|
||||
|
||||
/// success based dynamic routing
|
||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||
pub async fn perform_success_based_routing(
|
||||
state: &SessionState,
|
||||
routable_connectors: Vec<api_routing::RoutableConnectorChoice>,
|
||||
business_profile: &domain::Profile,
|
||||
) -> RoutingResult<Vec<api_routing::RoutableConnectorChoice>> {
|
||||
let success_based_dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef =
|
||||
business_profile
|
||||
.dynamic_routing_algorithm
|
||||
.clone()
|
||||
.map(|val| val.parse_value("DynamicRoutingAlgorithmRef"))
|
||||
.transpose()
|
||||
.change_context(errors::RoutingError::DeserializationError {
|
||||
from: "JSON".to_string(),
|
||||
to: "DynamicRoutingAlgorithmRef".to_string(),
|
||||
})
|
||||
.attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")?
|
||||
.unwrap_or_default();
|
||||
|
||||
let success_based_algo_ref = success_based_dynamic_routing_algo_ref
|
||||
.success_based_algorithm
|
||||
.ok_or(errors::RoutingError::GenericNotFoundError { field: "success_based_algorithm".to_string() })
|
||||
.attach_printable(
|
||||
"success_based_algorithm not found in dynamic_routing_algorithm from business_profile table",
|
||||
)?;
|
||||
|
||||
if success_based_algo_ref.enabled_feature
|
||||
== api_routing::SuccessBasedRoutingFeatures::DynamicConnectorSelection
|
||||
{
|
||||
logger::debug!(
|
||||
"performing success_based_routing for profile {}",
|
||||
business_profile.get_id().get_string_repr()
|
||||
);
|
||||
let client = state
|
||||
.grpc_client
|
||||
.dynamic_routing
|
||||
.success_rate_client
|
||||
.as_ref()
|
||||
.ok_or(errors::RoutingError::SuccessRateClientInitializationError)
|
||||
.attach_printable("success_rate gRPC client not found")?;
|
||||
|
||||
let success_based_routing_configs = routing::helpers::fetch_success_based_routing_configs(
|
||||
state,
|
||||
business_profile,
|
||||
success_based_algo_ref
|
||||
.algorithm_id_with_timestamp
|
||||
.algorithm_id
|
||||
.ok_or(errors::RoutingError::GenericNotFoundError {
|
||||
field: "success_based_routing_algorithm_id".to_string(),
|
||||
})
|
||||
.attach_printable(
|
||||
"success_based_routing_algorithm_id not found in business_profile",
|
||||
)?,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::RoutingError::SuccessBasedRoutingConfigError)
|
||||
.attach_printable("unable to fetch success_rate based dynamic routing configs")?;
|
||||
|
||||
let tenant_business_profile_id = routing::helpers::generate_tenant_business_profile_id(
|
||||
&state.tenant.redis_key_prefix,
|
||||
business_profile.get_id().get_string_repr(),
|
||||
);
|
||||
|
||||
let success_based_connectors: CalSuccessRateResponse = client
|
||||
.calculate_success_rate(
|
||||
tenant_business_profile_id,
|
||||
success_based_routing_configs,
|
||||
routable_connectors,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::RoutingError::SuccessRateCalculationError)
|
||||
.attach_printable(
|
||||
"unable to calculate/fetch success rate from dynamic routing service",
|
||||
)?;
|
||||
|
||||
let mut connectors = Vec::with_capacity(success_based_connectors.labels_with_score.len());
|
||||
for label_with_score in success_based_connectors.labels_with_score {
|
||||
let (connector, merchant_connector_id) = label_with_score.label
|
||||
.split_once(':')
|
||||
.ok_or(errors::RoutingError::InvalidSuccessBasedConnectorLabel(label_with_score.label.to_string()))
|
||||
.attach_printable(
|
||||
"unable to split connector_name and mca_id from the label obtained by the dynamic routing service",
|
||||
)?;
|
||||
connectors.push(api_routing::RoutableConnectorChoice {
|
||||
choice_kind: api_routing::RoutableChoiceKind::FullStruct,
|
||||
connector: common_enums::RoutableConnectors::from_str(connector)
|
||||
.change_context(errors::RoutingError::GenericConversionError {
|
||||
from: "String".to_string(),
|
||||
to: "RoutableConnectors".to_string(),
|
||||
})
|
||||
.attach_printable("unable to convert String to RoutableConnectors")?,
|
||||
merchant_connector_id: Some(
|
||||
common_utils::id_type::MerchantConnectorAccountId::wrap(
|
||||
merchant_connector_id.to_string(),
|
||||
)
|
||||
.change_context(errors::RoutingError::GenericConversionError {
|
||||
from: "String".to_string(),
|
||||
to: "MerchantConnectorAccountId".to_string(),
|
||||
})
|
||||
.attach_printable("unable to convert MerchantConnectorAccountId from string")?,
|
||||
),
|
||||
});
|
||||
}
|
||||
logger::debug!(success_based_routing_connectors=?connectors);
|
||||
Ok(connectors)
|
||||
} else {
|
||||
Ok(routable_connectors)
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,9 +441,13 @@ pub async fn link_routing_config(
|
||||
utils::when(
|
||||
matches!(
|
||||
dynamic_routing_ref.success_based_algorithm,
|
||||
Some(routing_types::DynamicAlgorithmWithTimestamp {
|
||||
algorithm_id: Some(ref id),
|
||||
timestamp: _
|
||||
Some(routing::SuccessBasedAlgorithm {
|
||||
algorithm_id_with_timestamp:
|
||||
routing_types::DynamicAlgorithmWithTimestamp {
|
||||
algorithm_id: Some(ref id),
|
||||
timestamp: _
|
||||
},
|
||||
enabled_feature: _
|
||||
}) if id == &algorithm_id
|
||||
),
|
||||
|| {
|
||||
@ -453,7 +457,17 @@ pub async fn link_routing_config(
|
||||
},
|
||||
)?;
|
||||
|
||||
dynamic_routing_ref.update_algorithm_id(algorithm_id);
|
||||
dynamic_routing_ref.update_algorithm_id(
|
||||
algorithm_id,
|
||||
dynamic_routing_ref
|
||||
.success_based_algorithm
|
||||
.clone()
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"missing success_based_algorithm in dynamic_algorithm_ref from business_profile table",
|
||||
)?
|
||||
.enabled_feature,
|
||||
);
|
||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||
db,
|
||||
key_manager_state,
|
||||
@ -1169,7 +1183,7 @@ pub async fn toggle_success_based_routing(
|
||||
state: SessionState,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
status: bool,
|
||||
feature_to_enable: routing::SuccessBasedRoutingFeatures,
|
||||
profile_id: common_utils::id_type::ProfileId,
|
||||
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
|
||||
metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(
|
||||
@ -1205,115 +1219,158 @@ pub async fn toggle_success_based_routing(
|
||||
)?
|
||||
.unwrap_or_default();
|
||||
|
||||
if status {
|
||||
let default_success_based_routing_config = routing::SuccessBasedRoutingConfig::default();
|
||||
let algorithm_id = common_utils::generate_routing_id_of_default_length();
|
||||
let timestamp = common_utils::date_time::now();
|
||||
let algo = RoutingAlgorithm {
|
||||
algorithm_id: algorithm_id.clone(),
|
||||
profile_id: business_profile.get_id().to_owned(),
|
||||
merchant_id: merchant_account.get_id().to_owned(),
|
||||
name: "Dynamic routing algorithm".to_string(),
|
||||
description: None,
|
||||
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
|
||||
algorithm_data: serde_json::json!(default_success_based_routing_config),
|
||||
created_at: timestamp,
|
||||
modified_at: timestamp,
|
||||
algorithm_for: common_enums::TransactionType::Payment,
|
||||
};
|
||||
match feature_to_enable {
|
||||
routing::SuccessBasedRoutingFeatures::Metrics
|
||||
| routing::SuccessBasedRoutingFeatures::DynamicConnectorSelection => {
|
||||
if let Some(ref mut algo_with_timestamp) =
|
||||
success_based_dynamic_routing_algo_ref.success_based_algorithm
|
||||
{
|
||||
match algo_with_timestamp
|
||||
.algorithm_id_with_timestamp
|
||||
.algorithm_id
|
||||
.clone()
|
||||
{
|
||||
Some(algorithm_id) => {
|
||||
// algorithm is already present in profile
|
||||
if algo_with_timestamp.enabled_feature == feature_to_enable {
|
||||
// algorithm already has the required feature
|
||||
Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: "Success rate based routing is already enabled"
|
||||
.to_string(),
|
||||
})?
|
||||
} else {
|
||||
// enable the requested feature for the algorithm
|
||||
algo_with_timestamp.update_enabled_features(feature_to_enable);
|
||||
let record = db
|
||||
.find_routing_algorithm_by_profile_id_algorithm_id(
|
||||
business_profile.get_id(),
|
||||
&algorithm_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(
|
||||
errors::ApiErrorResponse::ResourceIdNotFound,
|
||||
)?;
|
||||
let response = record.foreign_into();
|
||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||
db,
|
||||
key_manager_state,
|
||||
&key_store,
|
||||
business_profile,
|
||||
success_based_dynamic_routing_algo_ref,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let record = db
|
||||
.insert_routing_algorithm(algo)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Unable to insert record in routing algorithm table")?;
|
||||
|
||||
success_based_dynamic_routing_algo_ref.update_algorithm_id(algorithm_id);
|
||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||
db,
|
||||
key_manager_state,
|
||||
&key_store,
|
||||
business_profile,
|
||||
success_based_dynamic_routing_algo_ref,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let new_record = record.foreign_into();
|
||||
|
||||
metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]),
|
||||
);
|
||||
Ok(service_api::ApplicationResponse::Json(new_record))
|
||||
} else {
|
||||
let timestamp = common_utils::date_time::now_unix_timestamp();
|
||||
match success_based_dynamic_routing_algo_ref.success_based_algorithm {
|
||||
Some(algorithm_ref) => {
|
||||
if let Some(algorithm_id) = algorithm_ref.algorithm_id {
|
||||
let dynamic_routing_algorithm = routing_types::DynamicRoutingAlgorithmRef {
|
||||
success_based_algorithm: Some(
|
||||
routing_types::DynamicAlgorithmWithTimestamp {
|
||||
algorithm_id: None,
|
||||
timestamp,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
// redact cache for success based routing configs
|
||||
let cache_key = format!(
|
||||
"{}_{}",
|
||||
business_profile.get_id().get_string_repr(),
|
||||
algorithm_id.get_string_repr()
|
||||
);
|
||||
let cache_entries_to_redact =
|
||||
vec![cache::CacheKind::SuccessBasedDynamicRoutingCache(
|
||||
cache_key.into(),
|
||||
)];
|
||||
let _ = cache::publish_into_redact_channel(
|
||||
state.store.get_cache_store().as_ref(),
|
||||
cache_entries_to_redact,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
logger::error!(
|
||||
"unable to publish into the redact channel for evicting the success based routing config cache {e:?}"
|
||||
)
|
||||
});
|
||||
|
||||
let record = db
|
||||
.find_routing_algorithm_by_profile_id_algorithm_id(
|
||||
business_profile.get_id(),
|
||||
&algorithm_id,
|
||||
metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([(
|
||||
"profile_id",
|
||||
profile_id.get_string_repr().to_owned(),
|
||||
)]),
|
||||
);
|
||||
Ok(service_api::ApplicationResponse::Json(response))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// algorithm isn't present in profile
|
||||
helpers::default_success_based_routing_setup(
|
||||
&state,
|
||||
key_store,
|
||||
business_profile,
|
||||
feature_to_enable,
|
||||
merchant_account.get_id().to_owned(),
|
||||
success_based_dynamic_routing_algo_ref,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
|
||||
let response = record.foreign_into();
|
||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||
db,
|
||||
key_manager_state,
|
||||
&key_store,
|
||||
business_profile,
|
||||
dynamic_routing_algorithm,
|
||||
)
|
||||
.await?;
|
||||
|
||||
metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]),
|
||||
);
|
||||
|
||||
Ok(service_api::ApplicationResponse::Json(response))
|
||||
} else {
|
||||
Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: "Algorithm is already inactive".to_string(),
|
||||
})?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// algorithm isn't present in profile
|
||||
helpers::default_success_based_routing_setup(
|
||||
&state,
|
||||
key_store,
|
||||
business_profile,
|
||||
feature_to_enable,
|
||||
merchant_account.get_id().to_owned(),
|
||||
success_based_dynamic_routing_algo_ref,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
routing::SuccessBasedRoutingFeatures::None => {
|
||||
// disable success based routing for the requested profile
|
||||
let timestamp = common_utils::date_time::now_unix_timestamp();
|
||||
match success_based_dynamic_routing_algo_ref.success_based_algorithm {
|
||||
Some(algorithm_ref) => {
|
||||
if let Some(algorithm_id) =
|
||||
algorithm_ref.algorithm_id_with_timestamp.algorithm_id
|
||||
{
|
||||
let dynamic_routing_algorithm = routing_types::DynamicRoutingAlgorithmRef {
|
||||
success_based_algorithm: Some(routing::SuccessBasedAlgorithm {
|
||||
algorithm_id_with_timestamp:
|
||||
routing_types::DynamicAlgorithmWithTimestamp {
|
||||
algorithm_id: None,
|
||||
timestamp,
|
||||
},
|
||||
enabled_feature: routing::SuccessBasedRoutingFeatures::None,
|
||||
}),
|
||||
};
|
||||
|
||||
// redact cache for success based routing configs
|
||||
let cache_key = format!(
|
||||
"{}_{}",
|
||||
business_profile.get_id().get_string_repr(),
|
||||
algorithm_id.get_string_repr()
|
||||
);
|
||||
let cache_entries_to_redact =
|
||||
vec![cache::CacheKind::SuccessBasedDynamicRoutingCache(
|
||||
cache_key.into(),
|
||||
)];
|
||||
let _ = cache::publish_into_redact_channel(
|
||||
state.store.get_cache_store().as_ref(),
|
||||
cache_entries_to_redact,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("unable to publish into the redact channel for evicting the success based routing config cache")?;
|
||||
|
||||
let record = db
|
||||
.find_routing_algorithm_by_profile_id_algorithm_id(
|
||||
business_profile.get_id(),
|
||||
&algorithm_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
|
||||
let response = record.foreign_into();
|
||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||
db,
|
||||
key_manager_state,
|
||||
&key_store,
|
||||
business_profile,
|
||||
dynamic_routing_algorithm,
|
||||
)
|
||||
.await?;
|
||||
|
||||
metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([(
|
||||
"profile_id",
|
||||
profile_id.get_string_repr().to_owned(),
|
||||
)]),
|
||||
);
|
||||
|
||||
Ok(service_api::ApplicationResponse::Json(response))
|
||||
} else {
|
||||
Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: "Algorithm is already inactive".to_string(),
|
||||
})?
|
||||
}
|
||||
}
|
||||
None => Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: "Success rate based routing is already disabled".to_string(),
|
||||
})?,
|
||||
}
|
||||
None => Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: "Algorithm is already inactive".to_string(),
|
||||
})?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,16 @@ use api_models::routing as routing_types;
|
||||
use common_utils::ext_traits::ValueExt;
|
||||
use common_utils::{ext_traits::Encode, id_type, types::keymanager::KeyManagerState};
|
||||
use diesel_models::configs;
|
||||
#[cfg(feature = "v1")]
|
||||
use diesel_models::routing_algorithm;
|
||||
use error_stack::ResultExt;
|
||||
#[cfg(feature = "dynamic_routing")]
|
||||
use external_services::grpc_client::dynamic_routing::SuccessBasedDynamicRouting;
|
||||
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
||||
use external_services::grpc_client::dynamic_routing::SuccessBasedDynamicRouting;
|
||||
#[cfg(feature = "v1")]
|
||||
use hyperswitch_domain_models::api::ApplicationResponse;
|
||||
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
||||
use router_env::logger;
|
||||
#[cfg(any(feature = "dynamic_routing", feature = "v1"))]
|
||||
use router_env::{instrument, metrics::add_attributes, tracing};
|
||||
use rustc_hash::FxHashSet;
|
||||
use storage_impl::redis::cache;
|
||||
@ -29,8 +35,10 @@ use crate::{
|
||||
types::{domain, storage},
|
||||
utils::StringExt,
|
||||
};
|
||||
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
||||
use crate::{core::metrics as core_metrics, routes::metrics};
|
||||
#[cfg(feature = "v1")]
|
||||
use crate::{core::metrics as core_metrics, routes::metrics, types::transformers::ForeignInto};
|
||||
pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
|
||||
"Success rate based dynamic routing algorithm";
|
||||
|
||||
/// Provides us with all the configured configs of the Merchant in the ascending time configured
|
||||
/// manner and chooses the first of them
|
||||
@ -594,28 +602,8 @@ pub async fn refresh_success_based_routing_cache(
|
||||
pub async fn fetch_success_based_routing_configs(
|
||||
state: &SessionState,
|
||||
business_profile: &domain::Profile,
|
||||
dynamic_routing_algorithm: serde_json::Value,
|
||||
success_based_routing_id: id_type::RoutingId,
|
||||
) -> RouterResult<routing_types::SuccessBasedRoutingConfig> {
|
||||
let dynamic_routing_algorithm_ref = dynamic_routing_algorithm
|
||||
.parse_value::<routing_types::DynamicRoutingAlgorithmRef>("DynamicRoutingAlgorithmRef")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("unable to parse dynamic_routing_algorithm_ref")?;
|
||||
|
||||
let success_based_routing_id = dynamic_routing_algorithm_ref
|
||||
.success_based_algorithm
|
||||
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "success_based_algorithm not found in dynamic_routing_algorithm_ref"
|
||||
.to_string(),
|
||||
})?
|
||||
.algorithm_id
|
||||
// error can be possible when the feature is toggled off.
|
||||
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: format!(
|
||||
"unable to find algorithm_id in success based algorithm config as the feature is disabled for profile_id: {}",
|
||||
business_profile.get_id().get_string_repr()
|
||||
),
|
||||
})?;
|
||||
|
||||
let key = format!(
|
||||
"{}_{}",
|
||||
business_profile.get_id().get_string_repr(),
|
||||
@ -657,156 +645,185 @@ pub async fn push_metrics_for_success_based_routing(
|
||||
payment_attempt: &storage::PaymentAttempt,
|
||||
routable_connectors: Vec<routing_types::RoutableConnectorChoice>,
|
||||
business_profile: &domain::Profile,
|
||||
dynamic_routing_algorithm: serde_json::Value,
|
||||
) -> RouterResult<()> {
|
||||
let client = state
|
||||
.grpc_client
|
||||
.dynamic_routing
|
||||
.success_rate_client
|
||||
.as_ref()
|
||||
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "success_rate gRPC client not found".to_string(),
|
||||
})?;
|
||||
let success_based_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("Failed to deserialize DynamicRoutingAlgorithmRef from JSON")?
|
||||
.unwrap_or_default();
|
||||
|
||||
let payment_connector = &payment_attempt.connector.clone().ok_or(
|
||||
errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "unable to derive payment connector from payment attempt".to_string(),
|
||||
},
|
||||
)?;
|
||||
let success_based_algo_ref = success_based_dynamic_routing_algo_ref
|
||||
.success_based_algorithm
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("success_based_algorithm not found in dynamic_routing_algorithm from business_profile table")?;
|
||||
|
||||
let success_based_routing_configs =
|
||||
fetch_success_based_routing_configs(state, business_profile, dynamic_routing_algorithm)
|
||||
if success_based_algo_ref.enabled_feature != routing_types::SuccessBasedRoutingFeatures::None {
|
||||
let client = state
|
||||
.grpc_client
|
||||
.dynamic_routing
|
||||
.success_rate_client
|
||||
.as_ref()
|
||||
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "success_rate gRPC client not found".to_string(),
|
||||
})?;
|
||||
|
||||
let payment_connector = &payment_attempt.connector.clone().ok_or(
|
||||
errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "unable to derive payment connector from payment attempt".to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let success_based_routing_configs = fetch_success_based_routing_configs(
|
||||
state,
|
||||
business_profile,
|
||||
success_based_algo_ref
|
||||
.algorithm_id_with_timestamp
|
||||
.algorithm_id
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"success_based_routing_algorithm_id not found in business_profile",
|
||||
)?,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("unable to retrieve success_rate based dynamic routing configs")?;
|
||||
|
||||
let tenant_business_profile_id = generate_tenant_business_profile_id(
|
||||
&state.tenant.redis_key_prefix,
|
||||
business_profile.get_id().get_string_repr(),
|
||||
);
|
||||
|
||||
let success_based_connectors = client
|
||||
.calculate_success_rate(
|
||||
tenant_business_profile_id.clone(),
|
||||
success_based_routing_configs.clone(),
|
||||
routable_connectors.clone(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("unable to retrieve success_rate based dynamic routing configs")?;
|
||||
.attach_printable(
|
||||
"unable to calculate/fetch success rate from dynamic routing service",
|
||||
)?;
|
||||
|
||||
let tenant_business_profile_id = format!(
|
||||
"{}:{}",
|
||||
state.tenant.redis_key_prefix,
|
||||
business_profile.get_id().get_string_repr()
|
||||
);
|
||||
let payment_status_attribute =
|
||||
get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status);
|
||||
|
||||
let success_based_connectors = client
|
||||
.calculate_success_rate(
|
||||
tenant_business_profile_id.clone(),
|
||||
success_based_routing_configs.clone(),
|
||||
routable_connectors.clone(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("unable to calculate/fetch success rate from dynamic routing service")?;
|
||||
let first_success_based_connector_label = &success_based_connectors
|
||||
.labels_with_score
|
||||
.first()
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
|
||||
)?
|
||||
.label
|
||||
.to_string();
|
||||
|
||||
let payment_status_attribute =
|
||||
get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status);
|
||||
let (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label
|
||||
.split_once(':')
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"unable to split connector_name and mca_id from the first connector obtained from dynamic routing service",
|
||||
)?;
|
||||
|
||||
let first_success_based_connector_label = &success_based_connectors
|
||||
.labels_with_score
|
||||
.first()
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
|
||||
)?
|
||||
.label
|
||||
.to_string();
|
||||
let outcome = get_success_based_metrics_outcome_for_payment(
|
||||
&payment_status_attribute,
|
||||
payment_connector.to_string(),
|
||||
first_success_based_connector.to_string(),
|
||||
);
|
||||
|
||||
let (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label
|
||||
.split_once(':')
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"unable to split connector_name and mca_id from the first connector obtained from dynamic routing service",
|
||||
)?;
|
||||
|
||||
let outcome = get_success_based_metrics_outcome_for_payment(
|
||||
&payment_status_attribute,
|
||||
payment_connector.to_string(),
|
||||
first_success_based_connector.to_string(),
|
||||
);
|
||||
|
||||
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([
|
||||
("tenant", state.tenant.name.clone()),
|
||||
(
|
||||
"merchant_id",
|
||||
payment_attempt.merchant_id.get_string_repr().to_string(),
|
||||
),
|
||||
(
|
||||
"profile_id",
|
||||
payment_attempt.profile_id.get_string_repr().to_string(),
|
||||
),
|
||||
("merchant_connector_id", merchant_connector_id.to_string()),
|
||||
(
|
||||
"payment_id",
|
||||
payment_attempt.payment_id.get_string_repr().to_string(),
|
||||
),
|
||||
(
|
||||
"success_based_routing_connector",
|
||||
first_success_based_connector.to_string(),
|
||||
),
|
||||
("payment_connector", payment_connector.to_string()),
|
||||
(
|
||||
"currency",
|
||||
payment_attempt
|
||||
.currency
|
||||
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
|
||||
),
|
||||
(
|
||||
"payment_method",
|
||||
payment_attempt.payment_method.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|payment_method| payment_method.to_string(),
|
||||
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([
|
||||
("tenant", state.tenant.name.clone()),
|
||||
(
|
||||
"merchant_id",
|
||||
payment_attempt.merchant_id.get_string_repr().to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"payment_method_type",
|
||||
payment_attempt.payment_method_type.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|payment_method_type| payment_method_type.to_string(),
|
||||
(
|
||||
"profile_id",
|
||||
payment_attempt.profile_id.get_string_repr().to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"capture_method",
|
||||
payment_attempt.capture_method.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|capture_method| capture_method.to_string(),
|
||||
("merchant_connector_id", merchant_connector_id.to_string()),
|
||||
(
|
||||
"payment_id",
|
||||
payment_attempt.payment_id.get_string_repr().to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"authentication_type",
|
||||
payment_attempt.authentication_type.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|authentication_type| authentication_type.to_string(),
|
||||
(
|
||||
"success_based_routing_connector",
|
||||
first_success_based_connector.to_string(),
|
||||
),
|
||||
),
|
||||
("payment_status", payment_attempt.status.to_string()),
|
||||
("conclusive_classification", outcome.to_string()),
|
||||
]),
|
||||
);
|
||||
("payment_connector", payment_connector.to_string()),
|
||||
(
|
||||
"currency",
|
||||
payment_attempt
|
||||
.currency
|
||||
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
|
||||
),
|
||||
(
|
||||
"payment_method",
|
||||
payment_attempt.payment_method.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|payment_method| payment_method.to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"payment_method_type",
|
||||
payment_attempt.payment_method_type.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|payment_method_type| payment_method_type.to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"capture_method",
|
||||
payment_attempt.capture_method.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|capture_method| capture_method.to_string(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"authentication_type",
|
||||
payment_attempt.authentication_type.map_or_else(
|
||||
|| "None".to_string(),
|
||||
|authentication_type| authentication_type.to_string(),
|
||||
),
|
||||
),
|
||||
("payment_status", payment_attempt.status.to_string()),
|
||||
("conclusive_classification", outcome.to_string()),
|
||||
]),
|
||||
);
|
||||
logger::debug!("successfully pushed success_based_routing metrics");
|
||||
|
||||
client
|
||||
.update_success_rate(
|
||||
tenant_business_profile_id,
|
||||
success_based_routing_configs,
|
||||
vec![routing_types::RoutableConnectorChoiceWithStatus::new(
|
||||
routing_types::RoutableConnectorChoice {
|
||||
choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
|
||||
connector: common_enums::RoutableConnectors::from_str(
|
||||
payment_connector.as_str(),
|
||||
)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("unable to infer routable_connector from connector")?,
|
||||
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
|
||||
},
|
||||
payment_status_attribute == common_enums::AttemptStatus::Charged,
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"unable to update success based routing window in dynamic routing service",
|
||||
)?;
|
||||
Ok(())
|
||||
client
|
||||
.update_success_rate(
|
||||
tenant_business_profile_id,
|
||||
success_based_routing_configs,
|
||||
vec![routing_types::RoutableConnectorChoiceWithStatus::new(
|
||||
routing_types::RoutableConnectorChoice {
|
||||
choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
|
||||
connector: common_enums::RoutableConnectors::from_str(
|
||||
payment_connector.as_str(),
|
||||
)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("unable to infer routable_connector from connector")?,
|
||||
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
|
||||
},
|
||||
payment_status_attribute == common_enums::AttemptStatus::Charged,
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"unable to update success based routing window in dynamic routing service",
|
||||
)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||
@ -875,3 +892,67 @@ fn get_success_based_metrics_outcome_for_payment(
|
||||
_ => common_enums::SuccessBasedRoutingConclusiveState::NonDeterministic,
|
||||
}
|
||||
}
|
||||
|
||||
/// generates cache key with tenant's redis key prefix and profile_id
|
||||
pub fn generate_tenant_business_profile_id(
|
||||
redis_key_prefix: &str,
|
||||
business_profile_id: &str,
|
||||
) -> String {
|
||||
format!("{}:{}", redis_key_prefix, business_profile_id)
|
||||
}
|
||||
|
||||
/// default config setup for success_based_routing
|
||||
#[cfg(feature = "v1")]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn default_success_based_routing_setup(
|
||||
state: &SessionState,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
business_profile: domain::Profile,
|
||||
feature_to_enable: routing_types::SuccessBasedRoutingFeatures,
|
||||
merchant_id: id_type::MerchantId,
|
||||
mut success_based_dynamic_routing_algo: routing_types::DynamicRoutingAlgorithmRef,
|
||||
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
|
||||
let db = state.store.as_ref();
|
||||
let key_manager_state = &state.into();
|
||||
let profile_id = business_profile.get_id().to_owned();
|
||||
let default_success_based_routing_config = routing_types::SuccessBasedRoutingConfig::default();
|
||||
let algorithm_id = common_utils::generate_routing_id_of_default_length();
|
||||
let timestamp = common_utils::date_time::now();
|
||||
let algo = 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!(default_success_based_routing_config),
|
||||
created_at: timestamp,
|
||||
modified_at: timestamp,
|
||||
algorithm_for: common_enums::TransactionType::Payment,
|
||||
};
|
||||
|
||||
let record = db
|
||||
.insert_routing_algorithm(algo)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Unable to insert record in routing algorithm table")?;
|
||||
|
||||
success_based_dynamic_routing_algo.update_algorithm_id(algorithm_id, feature_to_enable);
|
||||
update_business_profile_active_dynamic_algorithm_ref(
|
||||
db,
|
||||
key_manager_state,
|
||||
&key_store,
|
||||
business_profile,
|
||||
success_based_dynamic_routing_algo,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let new_record = record.foreign_into();
|
||||
|
||||
core_metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([("profile_id", profile_id.get_string_repr().to_string())]),
|
||||
);
|
||||
Ok(ApplicationResponse::Json(new_record))
|
||||
}
|
||||
|
||||
@ -942,7 +942,7 @@ pub async fn toggle_success_based_routing(
|
||||
) -> impl Responder {
|
||||
let flow = Flow::ToggleDynamicRouting;
|
||||
let wrapper = routing_types::ToggleSuccessBasedRoutingWrapper {
|
||||
status: query.into_inner().status,
|
||||
feature_to_enable: query.into_inner().enable,
|
||||
profile_id: path.into_inner().profile_id,
|
||||
};
|
||||
Box::pin(oss_api::server_wrap(
|
||||
@ -958,7 +958,7 @@ pub async fn toggle_success_based_routing(
|
||||
state,
|
||||
auth.merchant_account,
|
||||
auth.key_store,
|
||||
wrapper.status,
|
||||
wrapper.feature_to_enable,
|
||||
wrapper.profile_id,
|
||||
)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user