diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index e2ccca46298..686328f698d 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -16,6 +16,7 @@ pub mod connector_onboarding; pub mod currency; pub mod customers; pub mod disputes; +pub mod dynamic_routing; pub mod encryption; pub mod errors; pub mod files; diff --git a/crates/router/src/core/dynamic_routing.rs b/crates/router/src/core/dynamic_routing.rs new file mode 100644 index 00000000000..2098f7a1a1b --- /dev/null +++ b/crates/router/src/core/dynamic_routing.rs @@ -0,0 +1,295 @@ +pub mod helpers; + +use api_models::{routing, routing as routing_types}; +use diesel_models::routing_algorithm::RoutingAlgorithm; +use error_stack::ResultExt; +#[cfg(feature = "v1")] +use router_env::logger; +use router_env::metrics::add_attributes; +#[cfg(feature = "v1")] +use storage_impl::redis::cache; + +#[cfg(feature = "v1")] +use crate::utils::ValueExt; +#[cfg(feature = "v2")] +use crate::{core::admin, utils::ValueExt}; +use crate::{ + core::{ + errors::{self, RouterResponse, StorageErrorExt}, + metrics, utils as core_utils, + }, + routes::SessionState, + services::api as service_api, + types::{domain, transformers::ForeignInto}, + utils::OptionExt, +}; + +#[cfg(feature = "v1")] +pub async fn toggle_success_based_routing( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + feature_to_enable: routing::SuccessBasedRoutingFeatures, + profile_id: common_utils::id_type::ProfileId, +) -> RouterResponse { + metrics::ROUTING_CREATE_REQUEST_RECEIVED.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + 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, + &key_store, + Some(&profile_id), + merchant_account.get_id(), + ) + .await? + .get_required_value("Profile") + .change_context(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id.get_string_repr().to_owned(), + })?; + + let mut 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( + "unable to deserialize dynamic routing algorithm ref from business profile", + )? + .unwrap_or_default(); + + 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?; + + 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 + } + } + } 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(), + })?, + } + } + } +} + +#[cfg(feature = "v1")] +pub async fn success_based_routing_update_configs( + state: SessionState, + request: routing_types::SuccessBasedRoutingConfig, + algorithm_id: common_utils::id_type::RoutingId, + profile_id: common_utils::id_type::ProfileId, +) -> RouterResponse { + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + let db = state.store.as_ref(); + + let dynamic_routing_algo_to_update = db + .find_routing_algorithm_by_profile_id_algorithm_id(&profile_id, &algorithm_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let mut config_to_update: routing::SuccessBasedRoutingConfig = dynamic_routing_algo_to_update + .algorithm_data + .parse_value::("SuccessBasedRoutingConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize algorithm data from routing table into SuccessBasedRoutingConfig")?; + + config_to_update.update(request); + + let updated_algorithm_id = common_utils::generate_routing_id_of_default_length(); + let timestamp = common_utils::date_time::now(); + let algo = RoutingAlgorithm { + algorithm_id: updated_algorithm_id, + profile_id: dynamic_routing_algo_to_update.profile_id, + merchant_id: dynamic_routing_algo_to_update.merchant_id, + name: dynamic_routing_algo_to_update.name, + description: dynamic_routing_algo_to_update.description, + kind: dynamic_routing_algo_to_update.kind, + algorithm_data: serde_json::json!(config_to_update), + created_at: timestamp, + modified_at: timestamp, + algorithm_for: dynamic_routing_algo_to_update.algorithm_for, + }; + let record = db + .insert_routing_algorithm(algo) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to insert record in routing algorithm table")?; + + // redact cache for success based routing configs + let cache_key = format!( + "{}_{}", + profile_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 new_record = record.foreign_into(); + + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + Ok(service_api::ApplicationResponse::Json(new_record)) +} diff --git a/crates/router/src/core/dynamic_routing/helpers.rs b/crates/router/src/core/dynamic_routing/helpers.rs new file mode 100644 index 00000000000..c23e0ffa67b --- /dev/null +++ b/crates/router/src/core/dynamic_routing/helpers.rs @@ -0,0 +1,544 @@ +#[cfg(all(feature = "dynamic_routing", feature = "v1"))] +use std::str::FromStr; +#[cfg(any(feature = "dynamic_routing", feature = "v1"))] +use std::sync::Arc; + +use api_models::routing as routing_types; +#[cfg(all(feature = "dynamic_routing", feature = "v1"))] +use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::Encode, id_type, types::keymanager::KeyManagerState}; +#[cfg(feature = "v1")] +use diesel_models::routing_algorithm; +use error_stack::ResultExt; +#[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 storage_impl::redis::cache; + +#[cfg(feature = "v2")] +use crate::types::domain::MerchantConnectorAccount; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use crate::types::storage; +use crate::{ + core::errors::{self, RouterResult}, + db::StorageInterface, + routes::SessionState, + types::domain, +}; +#[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"; + +/// Retrieves cached success_based routing configs specific to tenant and profile +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +pub async fn get_cached_success_based_routing_config_for_profile<'a>( + state: &SessionState, + key: &str, +) -> Option> { + cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE + .get_val::>(cache::CacheKey { + key: key.to_string(), + prefix: state.tenant.redis_key_prefix.clone(), + }) + .await +} + +/// Refreshes the cached success_based routing configs specific to tenant and profile +#[cfg(feature = "v1")] +pub async fn refresh_success_based_routing_cache( + state: &SessionState, + key: &str, + success_based_routing_config: routing_types::SuccessBasedRoutingConfig, +) -> Arc { + let config = Arc::new(success_based_routing_config); + cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE + .push( + cache::CacheKey { + key: key.to_string(), + prefix: state.tenant.redis_key_prefix.clone(), + }, + config.clone(), + ) + .await; + config +} + +/// Checked fetch of success based routing configs +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn fetch_success_based_routing_configs( + state: &SessionState, + business_profile: &domain::Profile, + success_based_routing_id: id_type::RoutingId, +) -> RouterResult { + let key = format!( + "{}_{}", + business_profile.get_id().get_string_repr(), + success_based_routing_id.get_string_repr() + ); + + if let Some(config) = + get_cached_success_based_routing_config_for_profile(state, key.as_str()).await + { + Ok(config.as_ref().clone()) + } else { + let success_rate_algorithm = state + .store + .find_routing_algorithm_by_profile_id_algorithm_id( + business_profile.get_id(), + &success_based_routing_id, + ) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("unable to retrieve success_rate_algorithm for profile from db")?; + + let success_rate_config = success_rate_algorithm + .algorithm_data + .parse_value::("SuccessBasedRoutingConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse success_based_routing_config struct")?; + + refresh_success_based_routing_cache(state, key.as_str(), success_rate_config.clone()).await; + + Ok(success_rate_config) + } +} + +#[cfg(feature = "v1")] +pub async fn update_business_profile_active_dynamic_algorithm_ref( + db: &dyn StorageInterface, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_business_profile: domain::Profile, + dynamic_routing_algorithm: routing_types::DynamicRoutingAlgorithmRef, +) -> RouterResult<()> { + let ref_val = dynamic_routing_algorithm + .encode_to_value() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert dynamic routing ref to value")?; + let business_profile_update = domain::ProfileUpdate::DynamicRoutingAlgorithmUpdate { + dynamic_routing_algorithm: Some(ref_val), + }; + db.update_profile_by_profile_id( + key_manager_state, + merchant_key_store, + current_business_profile, + business_profile_update, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update dynamic routing algorithm ref in business profile")?; + Ok(()) +} + +/// metrics for success based dynamic routing +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn push_metrics_with_update_window_for_success_based_routing( + state: &SessionState, + payment_attempt: &storage::PaymentAttempt, + routable_connectors: Vec, + business_profile: &domain::Profile, + success_based_routing_config_params_interpolator: SuccessBasedRoutingConfigParamsInterpolator, +) -> RouterResult<()> { + 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 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")?; + + 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_routing_config_params = success_based_routing_config_params_interpolator + .get_string_val( + success_based_routing_configs + .params + .as_ref() + .ok_or(errors::RoutingError::SuccessBasedRoutingParamsNotFoundError) + .change_context(errors::ApiErrorResponse::InternalServerError)?, + ); + + let success_based_connectors = client + .calculate_success_rate( + tenant_business_profile_id.clone(), + success_based_routing_configs.clone(), + success_based_routing_config_params.clone(), + routable_connectors.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to calculate/fetch success rate from dynamic routing service", + )?; + + let payment_status_attribute = + get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status); + + 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 (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label + .split_once(':') + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service", + first_success_based_connector_label + ))?; + + 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.tenant_id.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(), + ), + ), + ( + "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, + success_based_routing_config_params, + 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"))] +fn get_desired_payment_status_for_success_routing_metrics( + attempt_status: &common_enums::AttemptStatus, +) -> common_enums::AttemptStatus { + match attempt_status { + common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable => { + common_enums::AttemptStatus::Charged + } + common_enums::AttemptStatus::Failure + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::RouterDeclined => common_enums::AttemptStatus::Failure, + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => { + common_enums::AttemptStatus::Pending + } + } +} + +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +fn get_success_based_metrics_outcome_for_payment( + payment_status_attribute: &common_enums::AttemptStatus, + payment_connector: String, + first_success_based_connector: String, +) -> common_enums::SuccessBasedRoutingConclusiveState { + match payment_status_attribute { + common_enums::AttemptStatus::Charged + if *first_success_based_connector == *payment_connector => + { + common_enums::SuccessBasedRoutingConclusiveState::TruePositive + } + common_enums::AttemptStatus::Failure + if *first_success_based_connector == *payment_connector => + { + common_enums::SuccessBasedRoutingConclusiveState::FalsePositive + } + common_enums::AttemptStatus::Failure + if *first_success_based_connector != *payment_connector => + { + common_enums::SuccessBasedRoutingConclusiveState::TrueNegative + } + common_enums::AttemptStatus::Charged + if *first_success_based_connector != *payment_connector => + { + common_enums::SuccessBasedRoutingConclusiveState::FalseNegative + } + _ => 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> { + 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)) +} + +pub struct SuccessBasedRoutingConfigParamsInterpolator { + pub payment_method: Option, + pub payment_method_type: Option, + pub authentication_type: Option, + pub currency: Option, + pub country: Option, + pub card_network: Option, + pub card_bin: Option, +} + +impl SuccessBasedRoutingConfigParamsInterpolator { + pub fn new( + payment_method: Option, + payment_method_type: Option, + authentication_type: Option, + currency: Option, + country: Option, + card_network: Option, + card_bin: Option, + ) -> Self { + Self { + payment_method, + payment_method_type, + authentication_type, + currency, + country, + card_network, + card_bin, + } + } + + pub fn get_string_val( + &self, + params: &Vec, + ) -> String { + let mut parts: Vec = Vec::new(); + for param in params { + let val = match param { + routing_types::SuccessBasedRoutingConfigParams::PaymentMethod => self + .payment_method + .as_ref() + .map_or(String::new(), |pm| pm.to_string()), + routing_types::SuccessBasedRoutingConfigParams::PaymentMethodType => self + .payment_method_type + .as_ref() + .map_or(String::new(), |pmt| pmt.to_string()), + routing_types::SuccessBasedRoutingConfigParams::AuthenticationType => self + .authentication_type + .as_ref() + .map_or(String::new(), |at| at.to_string()), + routing_types::SuccessBasedRoutingConfigParams::Currency => self + .currency + .as_ref() + .map_or(String::new(), |cur| cur.to_string()), + routing_types::SuccessBasedRoutingConfigParams::Country => self + .country + .as_ref() + .map_or(String::new(), |cn| cn.to_string()), + routing_types::SuccessBasedRoutingConfigParams::CardNetwork => { + self.card_network.clone().unwrap_or_default() + } + routing_types::SuccessBasedRoutingConfigParams::CardBin => { + self.card_bin.clone().unwrap_or_default() + } + }; + if !val.is_empty() { + parts.push(val); + } + } + parts.join(":") + } +} diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 61572763af2..62a3334ef92 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -71,11 +71,11 @@ use self::{ use super::{ errors::StorageErrorExt, payment_methods::surcharge_decision_configs, routing::TransactionData, }; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use crate::core::dynamic_routing::helpers as dynamic_routing_helpers; #[cfg(feature = "frm")] use crate::core::fraud_check as frm_core; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -use crate::core::routing::helpers as routing_helpers; -#[cfg(all(feature = "v1", feature = "dynamic_routing"))] use crate::types::api::convert_connector_data_to_routable_connectors; use crate::{ configs::settings::{ApplePayPreDecryptFlow, PaymentMethodTypeTokenFilter}, @@ -5593,7 +5593,7 @@ where let connectors = { if business_profile.dynamic_routing_algorithm.is_some() { let success_based_routing_config_params_interpolator = - routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + dynamic_routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( payment_data.get_payment_attempt().payment_method, payment_data.get_payment_attempt().payment_method_type, payment_data.get_payment_attempt().authentication_type, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 427a406dbf5..5c8db47fa7f 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -21,7 +21,7 @@ use tracing_futures::Instrument; use super::{Operation, OperationSessionSetters, PostUpdateTracker}; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -use crate::core::routing::helpers as routing_helpers; +use crate::core::dynamic_routing::helpers as dynamic_routing_helpers; use crate::{ connector::utils::PaymentResponseRouterData, consts, @@ -1969,7 +1969,7 @@ async fn payment_response_update_tracker( let business_profile = business_profile.clone(); let payment_attempt = payment_attempt.clone(); let success_based_routing_config_params_interpolator = - routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + dynamic_routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( payment_attempt.payment_method, payment_attempt.payment_method_type, payment_attempt.authentication_type, @@ -2000,7 +2000,7 @@ async fn payment_response_update_tracker( ); tokio::spawn( async move { - routing_helpers::push_metrics_with_update_window_for_success_based_routing( + dynamic_routing_helpers::push_metrics_with_update_window_for_success_based_routing( &state, &payment_attempt, routable_connectors, diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 28321529ef2..da519d82833 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -42,6 +42,8 @@ use storage_impl::redis::cache::{CacheKey, CGRAPH_CACHE, ROUTING_CACHE}; #[cfg(feature = "v2")] use crate::core::admin; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use crate::core::dynamic_routing; #[cfg(feature = "payouts")] use crate::core::payouts; use crate::{ @@ -1240,7 +1242,7 @@ pub async fn perform_success_based_routing( state: &SessionState, routable_connectors: Vec, business_profile: &domain::Profile, - success_based_routing_config_params_interpolator: routing::helpers::SuccessBasedRoutingConfigParamsInterpolator, + success_based_routing_config_params_interpolator: dynamic_routing::helpers::SuccessBasedRoutingConfigParamsInterpolator, ) -> RoutingResult> { let success_based_dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef = business_profile @@ -1277,22 +1279,23 @@ pub async fn perform_success_based_routing( .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 success_based_routing_configs = + dynamic_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 success_based_routing_config_params = success_based_routing_config_params_interpolator .get_string_val( @@ -1302,10 +1305,11 @@ pub async fn perform_success_based_routing( .ok_or(errors::RoutingError::SuccessBasedRoutingParamsNotFoundError)?, ); - 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 tenant_business_profile_id = + dynamic_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( diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 0bd38918ee7..bbd89f2960b 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -10,16 +10,12 @@ use async_trait::async_trait; use diesel_models::routing_algorithm::RoutingAlgorithm; use error_stack::ResultExt; use hyperswitch_domain_models::{mandates, payment_address}; -#[cfg(feature = "v1")] -use router_env::logger; -use router_env::metrics::add_attributes; use rustc_hash::FxHashSet; -#[cfg(feature = "v1")] -use storage_impl::redis::cache; #[cfg(feature = "payouts")] use super::payouts; use super::{ + dynamic_routing::helpers as dynamic_routing_helpers, errors::RouterResult, payments::{ routing::{self as payments_routing}, @@ -468,7 +464,7 @@ pub async fn link_routing_config( )? .enabled_feature, ); - helpers::update_business_profile_active_dynamic_algorithm_ref( + dynamic_routing_helpers::update_business_profile_active_dynamic_algorithm_ref( db, key_manager_state, &key_store, @@ -1178,276 +1174,6 @@ pub async fn update_default_routing_config_for_profile( )) } -#[cfg(feature = "v1")] -pub async fn toggle_success_based_routing( - state: SessionState, - merchant_account: domain::MerchantAccount, - key_store: domain::MerchantKeyStore, - feature_to_enable: routing::SuccessBasedRoutingFeatures, - profile_id: common_utils::id_type::ProfileId, -) -> RouterResponse { - metrics::ROUTING_CREATE_REQUEST_RECEIVED.add( - &metrics::CONTEXT, - 1, - &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), - ); - 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, - &key_store, - Some(&profile_id), - merchant_account.get_id(), - ) - .await? - .get_required_value("Profile") - .change_context(errors::ApiErrorResponse::ProfileNotFound { - id: profile_id.get_string_repr().to_owned(), - })?; - - let mut 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( - "unable to deserialize dynamic routing algorithm ref from business profile", - )? - .unwrap_or_default(); - - 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?; - - 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 - } - } - } 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(), - })?, - } - } - } -} - -#[cfg(feature = "v1")] -pub async fn success_based_routing_update_configs( - state: SessionState, - request: routing_types::SuccessBasedRoutingConfig, - algorithm_id: common_utils::id_type::RoutingId, - profile_id: common_utils::id_type::ProfileId, -) -> RouterResponse { - metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add( - &metrics::CONTEXT, - 1, - &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), - ); - let db = state.store.as_ref(); - - let dynamic_routing_algo_to_update = db - .find_routing_algorithm_by_profile_id_algorithm_id(&profile_id, &algorithm_id) - .await - .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; - - let mut config_to_update: routing::SuccessBasedRoutingConfig = dynamic_routing_algo_to_update - .algorithm_data - .parse_value::("SuccessBasedRoutingConfig") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize algorithm data from routing table into SuccessBasedRoutingConfig")?; - - config_to_update.update(request); - - let updated_algorithm_id = common_utils::generate_routing_id_of_default_length(); - let timestamp = common_utils::date_time::now(); - let algo = RoutingAlgorithm { - algorithm_id: updated_algorithm_id, - profile_id: dynamic_routing_algo_to_update.profile_id, - merchant_id: dynamic_routing_algo_to_update.merchant_id, - name: dynamic_routing_algo_to_update.name, - description: dynamic_routing_algo_to_update.description, - kind: dynamic_routing_algo_to_update.kind, - algorithm_data: serde_json::json!(config_to_update), - created_at: timestamp, - modified_at: timestamp, - algorithm_for: dynamic_routing_algo_to_update.algorithm_for, - }; - let record = db - .insert_routing_algorithm(algo) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to insert record in routing algorithm table")?; - - // redact cache for success based routing configs - let cache_key = format!( - "{}_{}", - profile_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 new_record = record.foreign_into(); - - metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add( - &metrics::CONTEXT, - 1, - &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), - ); - Ok(service_api::ApplicationResponse::Json(new_record)) -} - #[async_trait] pub trait GetRoutableConnectorsForChoice { async fn get_routable_connectors( diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 0250d00d1bb..fc969263e0c 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -2,27 +2,10 @@ //! //! Functions that are used to perform the retrieval of merchant's //! routing dict, configs, defaults -#[cfg(all(feature = "dynamic_routing", feature = "v1"))] -use std::str::FromStr; -#[cfg(any(feature = "dynamic_routing", feature = "v1"))] -use std::sync::Arc; - use api_models::routing as routing_types; -#[cfg(all(feature = "dynamic_routing", feature = "v1"))] -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(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; @@ -35,10 +18,6 @@ use crate::{ types::{domain, storage}, utils::StringExt, }; -#[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 @@ -257,33 +236,6 @@ pub async fn update_profile_active_algorithm_ref( Ok(()) } -#[cfg(feature = "v1")] -pub async fn update_business_profile_active_dynamic_algorithm_ref( - db: &dyn StorageInterface, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - current_business_profile: domain::Profile, - dynamic_routing_algorithm: routing_types::DynamicRoutingAlgorithmRef, -) -> RouterResult<()> { - let ref_val = dynamic_routing_algorithm - .encode_to_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert dynamic routing ref to value")?; - let business_profile_update = domain::ProfileUpdate::DynamicRoutingAlgorithmUpdate { - dynamic_routing_algorithm: Some(ref_val), - }; - db.update_profile_by_profile_id( - key_manager_state, - merchant_key_store, - current_business_profile, - business_profile_update, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update dynamic routing algorithm ref in business profile")?; - Ok(()) -} - #[cfg(feature = "v2")] #[derive(Clone, Debug)] pub struct RoutingAlgorithmHelpers<'h> { @@ -561,484 +513,3 @@ pub fn get_default_config_key( storage::enums::TransactionType::Payout => format!("routing_default_po_{merchant_id}"), } } - -/// Retrieves cached success_based routing configs specific to tenant and profile -#[cfg(all(feature = "v1", feature = "dynamic_routing"))] -pub async fn get_cached_success_based_routing_config_for_profile<'a>( - state: &SessionState, - key: &str, -) -> Option> { - cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE - .get_val::>(cache::CacheKey { - key: key.to_string(), - prefix: state.tenant.redis_key_prefix.clone(), - }) - .await -} - -/// Refreshes the cached success_based routing configs specific to tenant and profile -#[cfg(feature = "v1")] -pub async fn refresh_success_based_routing_cache( - state: &SessionState, - key: &str, - success_based_routing_config: routing_types::SuccessBasedRoutingConfig, -) -> Arc { - let config = Arc::new(success_based_routing_config); - cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE - .push( - cache::CacheKey { - key: key.to_string(), - prefix: state.tenant.redis_key_prefix.clone(), - }, - config.clone(), - ) - .await; - config -} - -/// Checked fetch of success based routing configs -#[cfg(all(feature = "v1", feature = "dynamic_routing"))] -#[instrument(skip_all)] -pub async fn fetch_success_based_routing_configs( - state: &SessionState, - business_profile: &domain::Profile, - success_based_routing_id: id_type::RoutingId, -) -> RouterResult { - let key = format!( - "{}_{}", - business_profile.get_id().get_string_repr(), - success_based_routing_id.get_string_repr() - ); - - if let Some(config) = - get_cached_success_based_routing_config_for_profile(state, key.as_str()).await - { - Ok(config.as_ref().clone()) - } else { - let success_rate_algorithm = state - .store - .find_routing_algorithm_by_profile_id_algorithm_id( - business_profile.get_id(), - &success_based_routing_id, - ) - .await - .change_context(errors::ApiErrorResponse::ResourceIdNotFound) - .attach_printable("unable to retrieve success_rate_algorithm for profile from db")?; - - let success_rate_config = success_rate_algorithm - .algorithm_data - .parse_value::("SuccessBasedRoutingConfig") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to parse success_based_routing_config struct")?; - - refresh_success_based_routing_cache(state, key.as_str(), success_rate_config.clone()).await; - - Ok(success_rate_config) - } -} - -/// metrics for success based dynamic routing -#[cfg(all(feature = "v1", feature = "dynamic_routing"))] -#[instrument(skip_all)] -pub async fn push_metrics_with_update_window_for_success_based_routing( - state: &SessionState, - payment_attempt: &storage::PaymentAttempt, - routable_connectors: Vec, - business_profile: &domain::Profile, - success_based_routing_config_params_interpolator: SuccessBasedRoutingConfigParamsInterpolator, -) -> RouterResult<()> { - 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 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")?; - - 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_routing_config_params = success_based_routing_config_params_interpolator - .get_string_val( - success_based_routing_configs - .params - .as_ref() - .ok_or(errors::RoutingError::SuccessBasedRoutingParamsNotFoundError) - .change_context(errors::ApiErrorResponse::InternalServerError)?, - ); - - let success_based_connectors = client - .calculate_success_rate( - tenant_business_profile_id.clone(), - success_based_routing_configs.clone(), - success_based_routing_config_params.clone(), - routable_connectors.clone(), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "unable to calculate/fetch success rate from dynamic routing service", - )?; - - let payment_status_attribute = - get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status); - - 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 (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label - .split_once(':') - .ok_or(errors::ApiErrorResponse::InternalServerError) - .attach_printable(format!( - "unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service", - first_success_based_connector_label - ))?; - - 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.tenant_id.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(), - ), - ), - ( - "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, - success_based_routing_config_params, - 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"))] -fn get_desired_payment_status_for_success_routing_metrics( - attempt_status: &common_enums::AttemptStatus, -) -> common_enums::AttemptStatus { - match attempt_status { - common_enums::AttemptStatus::Charged - | common_enums::AttemptStatus::Authorized - | common_enums::AttemptStatus::PartialCharged - | common_enums::AttemptStatus::PartialChargedAndChargeable => { - common_enums::AttemptStatus::Charged - } - common_enums::AttemptStatus::Failure - | common_enums::AttemptStatus::AuthorizationFailed - | common_enums::AttemptStatus::AuthenticationFailed - | common_enums::AttemptStatus::CaptureFailed - | common_enums::AttemptStatus::RouterDeclined => common_enums::AttemptStatus::Failure, - common_enums::AttemptStatus::Started - | common_enums::AttemptStatus::AuthenticationPending - | common_enums::AttemptStatus::AuthenticationSuccessful - | common_enums::AttemptStatus::Authorizing - | common_enums::AttemptStatus::CodInitiated - | common_enums::AttemptStatus::Voided - | common_enums::AttemptStatus::VoidInitiated - | common_enums::AttemptStatus::CaptureInitiated - | common_enums::AttemptStatus::VoidFailed - | common_enums::AttemptStatus::AutoRefunded - | common_enums::AttemptStatus::Unresolved - | common_enums::AttemptStatus::Pending - | common_enums::AttemptStatus::PaymentMethodAwaited - | common_enums::AttemptStatus::ConfirmationAwaited - | common_enums::AttemptStatus::DeviceDataCollectionPending => { - common_enums::AttemptStatus::Pending - } - } -} - -#[cfg(all(feature = "v1", feature = "dynamic_routing"))] -fn get_success_based_metrics_outcome_for_payment( - payment_status_attribute: &common_enums::AttemptStatus, - payment_connector: String, - first_success_based_connector: String, -) -> common_enums::SuccessBasedRoutingConclusiveState { - match payment_status_attribute { - common_enums::AttemptStatus::Charged - if *first_success_based_connector == *payment_connector => - { - common_enums::SuccessBasedRoutingConclusiveState::TruePositive - } - common_enums::AttemptStatus::Failure - if *first_success_based_connector == *payment_connector => - { - common_enums::SuccessBasedRoutingConclusiveState::FalsePositive - } - common_enums::AttemptStatus::Failure - if *first_success_based_connector != *payment_connector => - { - common_enums::SuccessBasedRoutingConclusiveState::TrueNegative - } - common_enums::AttemptStatus::Charged - if *first_success_based_connector != *payment_connector => - { - common_enums::SuccessBasedRoutingConclusiveState::FalseNegative - } - _ => 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> { - 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)) -} - -pub struct SuccessBasedRoutingConfigParamsInterpolator { - pub payment_method: Option, - pub payment_method_type: Option, - pub authentication_type: Option, - pub currency: Option, - pub country: Option, - pub card_network: Option, - pub card_bin: Option, -} - -impl SuccessBasedRoutingConfigParamsInterpolator { - pub fn new( - payment_method: Option, - payment_method_type: Option, - authentication_type: Option, - currency: Option, - country: Option, - card_network: Option, - card_bin: Option, - ) -> Self { - Self { - payment_method, - payment_method_type, - authentication_type, - currency, - country, - card_network, - card_bin, - } - } - - pub fn get_string_val( - &self, - params: &Vec, - ) -> String { - let mut parts: Vec = Vec::new(); - for param in params { - let val = match param { - routing_types::SuccessBasedRoutingConfigParams::PaymentMethod => self - .payment_method - .as_ref() - .map_or(String::new(), |pm| pm.to_string()), - routing_types::SuccessBasedRoutingConfigParams::PaymentMethodType => self - .payment_method_type - .as_ref() - .map_or(String::new(), |pmt| pmt.to_string()), - routing_types::SuccessBasedRoutingConfigParams::AuthenticationType => self - .authentication_type - .as_ref() - .map_or(String::new(), |at| at.to_string()), - routing_types::SuccessBasedRoutingConfigParams::Currency => self - .currency - .as_ref() - .map_or(String::new(), |cur| cur.to_string()), - routing_types::SuccessBasedRoutingConfigParams::Country => self - .country - .as_ref() - .map_or(String::new(), |cn| cn.to_string()), - routing_types::SuccessBasedRoutingConfigParams::CardNetwork => { - self.card_network.clone().unwrap_or_default() - } - routing_types::SuccessBasedRoutingConfigParams::CardBin => { - self.card_bin.clone().unwrap_or_default() - } - }; - if !val.is_empty() { - parts.push(val); - } - } - parts.join(":") - } -} diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 29c75942556..ac3a360461f 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -11,7 +11,7 @@ use router_env::{ }; use crate::{ - core::{api_locking, conditional_config, routing, surcharge_decision_config}, + core::{api_locking, conditional_config, dynamic_routing, routing, surcharge_decision_config}, routes::AppState, services::{api as oss_api, authentication as auth, authorization::permissions::Permission}, }; @@ -1037,7 +1037,7 @@ pub async fn toggle_success_based_routing( auth: auth::AuthenticationData, wrapper: routing_types::ToggleSuccessBasedRoutingWrapper, _| { - routing::toggle_success_based_routing( + dynamic_routing::toggle_success_based_routing( state, auth.merchant_account, auth.key_store, @@ -1078,7 +1078,7 @@ pub async fn success_based_routing_update_configs( &req, routing_payload_wrapper, |state, _, wrapper: routing_types::SuccessBasedRoutingPayloadWrapper, _| async { - Box::pin(routing::success_based_routing_update_configs( + Box::pin(dynamic_routing::success_based_routing_update_configs( state, wrapper.updated_config, wrapper.algorithm_id,