mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +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:
		| @ -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)) | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Prajjwal Kumar
					Prajjwal Kumar