diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 2e570816ab4c..b6146794a9de 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -743,7 +743,7 @@ pub struct EliminationRoutingConfig { pub elimination_analyser_config: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, ToSchema)] pub struct EliminationAnalyserConfig { pub bucket_size: Option, pub bucket_leak_interval_in_secs: Option, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 193998c3fd1c..538a3184f3e1 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3537,6 +3537,17 @@ pub enum ErrorCategory { ProcessorDeclineIncorrectData, } +impl ErrorCategory { + pub fn should_perform_elimination_routing(self) -> bool { + match self { + Self::ProcessorDowntime | Self::ProcessorDeclineUnauthorized => true, + Self::IssueWithPaymentMethod + | Self::ProcessorDeclineIncorrectData + | Self::FrmDecline => false, + } + } +} + #[derive( Clone, Debug, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 619d2fb19375..bf368dbc3547 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true [features] default = ["common_default", "v1"] -common_default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "retry", "frm", "tls", "partial-auth", "km_forward_x_request_id"] +common_default = ["dynamic_routing", "kv_store", "stripe", "oltp", "olap", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "retry", "frm", "tls", "partial-auth", "km_forward_x_request_id"] olap = ["hyperswitch_domain_models/olap", "storage_impl/olap", "scheduler/olap", "api_models/olap", "dep:analytics"] tls = ["actix-web/rustls-0_22"] email = ["external_services/email", "scheduler/email", "olap"] diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 96321d097946..fcd06d3bfd2e 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -336,6 +336,18 @@ pub enum RoutingError { SuccessRateCalculationError, #[error("Success rate client from dynamic routing gRPC service not initialized")] SuccessRateClientInitializationError, + #[error("Elimintaion client from dynamic routing gRPC service not initialized")] + ElimintaionClientInitializationError, + #[error("Unable to analyze elimintaion routing config from dynamic routing service")] + ElimintaionRoutingCalculationError, + #[error("Params not found in elimination based routing config")] + EliminationBasedRoutingParamsNotFoundError, + #[error("Unable to retrieve elimination based routing config")] + EliminationRoutingConfigError, + #[error( + "Invalid elimination based connector label received from dynamic routing service: '{0}'" + )] + InvalidEliminationBasedConnectorLabel(String), #[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}'")] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e68bd09ba462..5bea394bc1fa 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -6199,8 +6199,8 @@ where .attach_printable("failed to perform volume split on routing type")?; if routing_choice.routing_type.is_dynamic_routing() { - let success_based_routing_config_params_interpolator = - routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + let dynamic_routing_config_params_interpolator = + routing_helpers::DynamicRoutingConfigParamsInterpolator::new( payment_data.get_payment_attempt().payment_method, payment_data.get_payment_attempt().payment_method_type, payment_data.get_payment_attempt().authentication_type, @@ -6230,15 +6230,50 @@ where .and_then(|card_isin| card_isin.as_str()) .map(|card_isin| card_isin.to_string()), ); - routing::perform_success_based_routing( - state, - connectors.clone(), - business_profile, - success_based_routing_config_params_interpolator, - ) - .await - .map_err(|e| logger::error!(success_rate_routing_error=?e)) - .unwrap_or(connectors) + let mut connectors = if dynamic_routing_config.success_based_algorithm.is_some_and( + |success_based_algo| { + success_based_algo + .algorithm_id_with_timestamp + .algorithm_id + .is_some() + }, + ) { + logger::info!("Performing success based routing from dynamic_routing service"); + routing::perform_success_based_routing( + state, + connectors.clone(), + business_profile, + dynamic_routing_config_params_interpolator.clone(), + ) + .await + .map_err(|e| logger::error!(success_rate_routing_error=?e)) + .unwrap_or(connectors) + } else { + connectors + }; + + connectors = if dynamic_routing_config + .elimination_routing_algorithm + .is_some_and(|elimination_routing_algo| { + elimination_routing_algo + .algorithm_id_with_timestamp + .algorithm_id + .is_some() + }) { + logger::info!("Performing elimination routing from dynamic_routing service"); + routing::perform_elimination_routing( + state, + connectors.clone(), + business_profile, + dynamic_routing_config_params_interpolator, + ) + .await + .map_err(|e| logger::error!(success_rate_routing_error=?e)) + .unwrap_or(connectors) + } else { + connectors + }; + connectors } else { connectors } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 988429a0eecb..1b12e70f17e2 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1311,7 +1311,10 @@ async fn payment_response_update_tracker( .as_mut() .map(|info| info.status = status) }); - let (capture_update, mut payment_attempt_update) = match router_data.response.clone() { + let (capture_update, mut payment_attempt_update, gsm_error_category) = match router_data + .response + .clone() + { Err(err) => { let auth_update = if Some(router_data.auth_type) != payment_data.payment_attempt.authentication_type @@ -1320,121 +1323,125 @@ async fn payment_response_update_tracker( } else { None }; - let (capture_update, attempt_update) = match payment_data.multiple_capture_data { - Some(multiple_capture_data) => { - let capture_update = storage::CaptureUpdate::ErrorUpdate { - status: match err.status_code { - 500..=511 => enums::CaptureStatus::Pending, - _ => enums::CaptureStatus::Failed, - }, - error_code: Some(err.code), - error_message: Some(err.message), - error_reason: err.reason, - }; - let capture_update_list = vec![( - multiple_capture_data.get_latest_capture().clone(), - capture_update, - )]; - ( - Some((multiple_capture_data, capture_update_list)), - auth_update.map(|auth_type| { - storage::PaymentAttemptUpdate::AuthenticationTypeUpdate { - authentication_type: auth_type, - updated_by: storage_scheme.to_string(), - } - }), - ) - } - None => { - let connector_name = router_data.connector.to_string(); - let flow_name = core_utils::get_flow_name::()?; - let option_gsm = payments_helpers::get_gsm_record( - state, - Some(err.code.clone()), - Some(err.message.clone()), - connector_name, - flow_name.clone(), - ) - .await; - - let gsm_unified_code = - option_gsm.as_ref().and_then(|gsm| gsm.unified_code.clone()); - let gsm_unified_message = option_gsm.and_then(|gsm| gsm.unified_message); - - let (unified_code, unified_message) = if let Some((code, message)) = - gsm_unified_code.as_ref().zip(gsm_unified_message.as_ref()) - { - (code.to_owned(), message.to_owned()) - } else { + let (capture_update, attempt_update, gsm_error_category) = + match payment_data.multiple_capture_data { + Some(multiple_capture_data) => { + let capture_update = storage::CaptureUpdate::ErrorUpdate { + status: match err.status_code { + 500..=511 => enums::CaptureStatus::Pending, + _ => enums::CaptureStatus::Failed, + }, + error_code: Some(err.code), + error_message: Some(err.message), + error_reason: err.reason, + }; + let capture_update_list = vec![( + multiple_capture_data.get_latest_capture().clone(), + capture_update, + )]; ( - consts::DEFAULT_UNIFIED_ERROR_CODE.to_owned(), - consts::DEFAULT_UNIFIED_ERROR_MESSAGE.to_owned(), + Some((multiple_capture_data, capture_update_list)), + auth_update.map(|auth_type| { + storage::PaymentAttemptUpdate::AuthenticationTypeUpdate { + authentication_type: auth_type, + updated_by: storage_scheme.to_string(), + } + }), + None, ) - }; - let unified_translated_message = locale - .as_ref() - .async_and_then(|locale_str| async { - payments_helpers::get_unified_translation( - state, - unified_code.to_owned(), - unified_message.to_owned(), - locale_str.to_owned(), + } + None => { + let connector_name = router_data.connector.to_string(); + let flow_name = core_utils::get_flow_name::()?; + let option_gsm = payments_helpers::get_gsm_record( + state, + Some(err.code.clone()), + Some(err.message.clone()), + connector_name, + flow_name.clone(), + ) + .await; + + let gsm_unified_code = + option_gsm.as_ref().and_then(|gsm| gsm.unified_code.clone()); + let gsm_unified_message = + option_gsm.clone().and_then(|gsm| gsm.unified_message); + + let (unified_code, unified_message) = if let Some((code, message)) = + gsm_unified_code.as_ref().zip(gsm_unified_message.as_ref()) + { + (code.to_owned(), message.to_owned()) + } else { + ( + consts::DEFAULT_UNIFIED_ERROR_CODE.to_owned(), + consts::DEFAULT_UNIFIED_ERROR_MESSAGE.to_owned(), ) + }; + let unified_translated_message = locale + .as_ref() + .async_and_then(|locale_str| async { + payments_helpers::get_unified_translation( + state, + unified_code.to_owned(), + unified_message.to_owned(), + locale_str.to_owned(), + ) + .await + }) .await - }) - .await - .or(Some(unified_message)); + .or(Some(unified_message)); - let status = match err.attempt_status { - // Use the status sent by connector in error_response if it's present - Some(status) => status, - None => - // mark previous attempt status for technical failures in PSync flow - { - if flow_name == "PSync" { - match err.status_code { - // marking failure for 2xx because this is genuine payment failure - 200..=299 => enums::AttemptStatus::Failure, - _ => router_data.status, - } - } else if flow_name == "Capture" { - match err.status_code { - 500..=511 => enums::AttemptStatus::Pending, - // don't update the status for 429 error status - 429 => router_data.status, - _ => enums::AttemptStatus::Failure, - } - } else { - match err.status_code { - 500..=511 => enums::AttemptStatus::Pending, - _ => enums::AttemptStatus::Failure, + let status = match err.attempt_status { + // Use the status sent by connector in error_response if it's present + Some(status) => status, + None => + // mark previous attempt status for technical failures in PSync flow + { + if flow_name == "PSync" { + match err.status_code { + // marking failure for 2xx because this is genuine payment failure + 200..=299 => enums::AttemptStatus::Failure, + _ => router_data.status, + } + } else if flow_name == "Capture" { + match err.status_code { + 500..=511 => enums::AttemptStatus::Pending, + // don't update the status for 429 error status + 429 => router_data.status, + _ => enums::AttemptStatus::Failure, + } + } else { + match err.status_code { + 500..=511 => enums::AttemptStatus::Pending, + _ => enums::AttemptStatus::Failure, + } } } - } - }; - ( - None, - Some(storage::PaymentAttemptUpdate::ErrorUpdate { - connector: None, - status, - error_message: Some(Some(err.message)), - error_code: Some(Some(err.code)), - error_reason: Some(err.reason), - amount_capturable: router_data - .request - .get_amount_capturable(&payment_data, status) - .map(MinorUnit::new), - updated_by: storage_scheme.to_string(), - unified_code: Some(Some(unified_code)), - unified_message: Some(unified_translated_message), - connector_transaction_id: err.connector_transaction_id, - payment_method_data: additional_payment_method_data, - authentication_type: auth_update, - }), - ) - } - }; - (capture_update, attempt_update) + }; + ( + None, + Some(storage::PaymentAttemptUpdate::ErrorUpdate { + connector: None, + status, + error_message: Some(Some(err.message)), + error_code: Some(Some(err.code)), + error_reason: Some(err.reason), + amount_capturable: router_data + .request + .get_amount_capturable(&payment_data, status) + .map(MinorUnit::new), + updated_by: storage_scheme.to_string(), + unified_code: Some(Some(unified_code)), + unified_message: Some(unified_translated_message), + connector_transaction_id: err.connector_transaction_id, + payment_method_data: additional_payment_method_data, + authentication_type: auth_update, + }), + option_gsm.and_then(|option_gsm| option_gsm.error_category), + ) + } + }; + (capture_update, attempt_update, gsm_error_category) } Ok(payments_response) => { @@ -1468,6 +1475,7 @@ async fn payment_response_update_tracker( payment_method_data: None, authentication_type: auth_update, }), + None, ) } Ok(()) => { @@ -1523,7 +1531,7 @@ async fn payment_response_update_tracker( updated_by: storage_scheme.to_string(), }; - (None, Some(payment_attempt_update)) + (None, Some(payment_attempt_update), None) } types::PaymentsResponseData::TransactionResponse { resource_id, @@ -1737,7 +1745,7 @@ async fn payment_response_update_tracker( ), }; - (capture_updates, payment_attempt_update) + (capture_updates, payment_attempt_update, None) } types::PaymentsResponseData::TransactionUnresolvedResponse { resource_id, @@ -1765,22 +1773,31 @@ async fn payment_response_update_tracker( connector_response_reference_id, updated_by: storage_scheme.to_string(), }), + None, ) } - types::PaymentsResponseData::SessionResponse { .. } => (None, None), - types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), - types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), + types::PaymentsResponseData::SessionResponse { .. } => (None, None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => { + (None, None, None) + } + types::PaymentsResponseData::TokenizationResponse { .. } => { + (None, None, None) + } types::PaymentsResponseData::ConnectorCustomerResponse { .. } => { - (None, None) + (None, None, None) } types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => { - (None, None) + (None, None, None) + } + types::PaymentsResponseData::PostProcessingResponse { .. } => { + (None, None, None) } - types::PaymentsResponseData::PostProcessingResponse { .. } => (None, None), types::PaymentsResponseData::IncrementalAuthorizationResponse { .. - } => (None, None), - types::PaymentsResponseData::SessionUpdateResponse { .. } => (None, None), + } => (None, None, None), + types::PaymentsResponseData::SessionUpdateResponse { .. } => { + (None, None, None) + } types::PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, } => match payment_data.multiple_capture_data { @@ -1789,9 +1806,13 @@ async fn payment_response_update_tracker( &multiple_capture_data, capture_sync_response_list, )?; - (Some((multiple_capture_data, capture_update_list)), None) + ( + Some((multiple_capture_data, capture_update_list)), + None, + None, + ) } - None => (None, None), + None => (None, None, None), }, } } @@ -1969,12 +1990,16 @@ async fn payment_response_update_tracker( #[cfg(all(feature = "v1", feature = "dynamic_routing"))] { - if business_profile.dynamic_routing_algorithm.is_some() { + if let Some(algo) = business_profile.dynamic_routing_algorithm.clone() { + let dynamic_routing_config: api_models::routing::DynamicRoutingAlgorithmRef = algo + .parse_value("DynamicRoutingAlgorithmRef") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")?; let state = state.clone(); let business_profile = business_profile.clone(); let payment_attempt = payment_attempt.clone(); - let success_based_routing_config_params_interpolator = - routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + let dynamic_routing_config_params_interpolator = + routing_helpers::DynamicRoutingConfigParamsInterpolator::new( payment_attempt.payment_method, payment_attempt.payment_method_type, payment_attempt.authentication_type, @@ -2005,16 +2030,49 @@ async fn payment_response_update_tracker( ); tokio::spawn( async move { - routing_helpers::push_metrics_with_update_window_for_success_based_routing( - &state, - &payment_attempt, - routable_connectors, - &business_profile, - success_based_routing_config_params_interpolator, - ) - .await - .map_err(|e| logger::error!(dynamic_routing_metrics_error=?e)) - .ok(); + if dynamic_routing_config.success_based_algorithm.is_some_and( + |success_based_algo| { + success_based_algo + .algorithm_id_with_timestamp + .algorithm_id + .is_some() + }, + ) { + routing_helpers::push_metrics_with_update_window_for_success_based_routing( + &state, + &payment_attempt, + routable_connectors.clone(), + &business_profile, + dynamic_routing_config_params_interpolator.clone(), + ) + .await + .map_err(|e| logger::error!(dynamic_routing_metrics_error=?e)) + .ok(); + }; + if let Some(gsm_error_category) = gsm_error_category { + if dynamic_routing_config + .elimination_routing_algorithm + .is_some_and(|elimination_algo| { + elimination_algo + .algorithm_id_with_timestamp + .algorithm_id + .is_some() + }) + && gsm_error_category.should_perform_elimination_routing() + { + logger::info!("Performing update window for elimination routing"); + routing_helpers::update_window_for_elimination_routing( + &state, + &payment_attempt, + &business_profile, + dynamic_routing_config_params_interpolator, + gsm_error_category, + ) + .await + .map_err(|e| logger::error!(dynamic_routing_metrics_error=?e)) + .ok(); + }; + }; } .in_current_span(), ); diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 6d3c0973283c..ec889531d8f2 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -23,8 +23,9 @@ use euclid::{ frontend::{ast, dir as euclid_dir}, }; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -use external_services::grpc_client::dynamic_routing::success_rate_client::{ - CalSuccessRateResponse, SuccessBasedDynamicRouting, +use external_services::grpc_client::dynamic_routing::{ + elimination_rate_client::{EliminationBasedRouting, EliminationResponse}, + success_rate_client::{CalSuccessRateResponse, SuccessBasedDynamicRouting}, }; use hyperswitch_domain_models::address::Address; use kgraph_utils::{ @@ -1270,7 +1271,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: routing::helpers::DynamicRoutingConfigParamsInterpolator, ) -> RoutingResult> { let success_based_dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef = business_profile @@ -1307,7 +1308,7 @@ 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( + let success_based_routing_configs = routing::helpers::fetch_success_based_routing_config( state, business_profile, success_based_algo_ref @@ -1385,3 +1386,138 @@ pub async fn perform_success_based_routing( Ok(routable_connectors) } } + +/// elimination dynamic routing +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +pub async fn perform_elimination_routing( + state: &SessionState, + routable_connectors: Vec, + business_profile: &domain::Profile, + elimination_routing_configs_params_interpolator: routing::helpers::DynamicRoutingConfigParamsInterpolator, +) -> RoutingResult> { + let 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 elimination_algo_ref = dynamic_routing_algo_ref + .elimination_routing_algorithm + .ok_or(errors::RoutingError::GenericNotFoundError { field: "elimintaion_algorithm".to_string() }) + .attach_printable( + "elimintaion_algorithm not found in dynamic_routing_algorithm from business_profile table", + )?; + + if elimination_algo_ref.enabled_feature + == api_routing::DynamicRoutingFeatures::DynamicConnectorSelection + { + logger::debug!( + "performing elimintaion_routing for profile {}", + business_profile.get_id().get_string_repr() + ); + let client = state + .grpc_client + .dynamic_routing + .elimination_rate_client + .as_ref() + .ok_or(errors::RoutingError::ElimintaionClientInitializationError) + .attach_printable("elimintaion routing's gRPC client not found")?; + + let elimination_routing_config = routing::helpers::fetch_elimintaion_routing_configs( + state, + business_profile, + elimination_algo_ref + .algorithm_id_with_timestamp + .algorithm_id + .ok_or(errors::RoutingError::GenericNotFoundError { + field: "elimination_routing_algorithm_id".to_string(), + }) + .attach_printable( + "elimintaion_routing_algorithm_id not found in business_profile", + )?, + ) + .await + .change_context(errors::RoutingError::EliminationRoutingConfigError) + .attach_printable("unable to fetch elimination dynamic routing configs")?; + + let elimination_routing_config_params = elimination_routing_configs_params_interpolator + .get_string_val( + elimination_routing_config + .params + .as_ref() + .ok_or(errors::RoutingError::EliminationBasedRoutingParamsNotFoundError)?, + ); + + 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 elimination_based_connectors: EliminationResponse = client + .perform_elimination_routing( + tenant_business_profile_id, + elimination_routing_config_params, + routable_connectors.clone(), + elimination_routing_config.elimination_analyser_config, + state.get_grpc_headers(), + ) + .await + .change_context(errors::RoutingError::ElimintaionRoutingCalculationError) + .attach_printable( + "unable to analyze/fetch elimintaion routing from dynamic routing service", + )?; + let mut connectors = + Vec::with_capacity(elimination_based_connectors.labels_with_status.len()); + let mut eliminated_connectors = + Vec::with_capacity(elimination_based_connectors.labels_with_status.len()); + let mut non_eliminated_connectors = + Vec::with_capacity(elimination_based_connectors.labels_with_status.len()); + for labels_with_status in elimination_based_connectors.labels_with_status { + let (connector, merchant_connector_id) = labels_with_status.label + .split_once(':') + .ok_or(errors::RoutingError::InvalidEliminationBasedConnectorLabel(labels_with_status.label.to_string())) + .attach_printable( + "unable to split connector_name and mca_id from the label obtained by the elimination based dynamic routing service", + )?; + + let routable_connector = 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")?, + ), + }; + + if labels_with_status.is_eliminated { + eliminated_connectors.push(routable_connector); + } else { + non_eliminated_connectors.push(routable_connector); + } + connectors.extend(non_eliminated_connectors.clone()); + connectors.extend(eliminated_connectors.clone()); + } + logger::debug!(eliminated_connectors=?eliminated_connectors); + logger::debug!(elimination_based_routing_connectors=?connectors); + Ok(connectors) + } else { + Ok(routable_connectors) + } +} diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 489c3122a8e9..5bc0811c1e05 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -18,7 +18,10 @@ use diesel_models::dynamic_routing_stats::DynamicRoutingStatsNew; use diesel_models::routing_algorithm; use error_stack::ResultExt; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] -use external_services::grpc_client::dynamic_routing::success_rate_client::SuccessBasedDynamicRouting; +use external_services::grpc_client::dynamic_routing::{ + elimination_rate_client::EliminationBasedRouting, + success_rate_client::SuccessBasedDynamicRouting, +}; #[cfg(feature = "v1")] use hyperswitch_domain_models::api::ApplicationResponse; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] @@ -602,10 +605,87 @@ pub async fn refresh_success_based_routing_cache( config } +/// Retrieves cached elimination routing configs specific to tenant and profile +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +pub async fn get_cached_elimination_routing_config_for_profile<'a>( + state: &SessionState, + key: &str, +) -> Option> { + cache::ELIMINATION_BASED_DYNAMIC_ALGORITHM_CACHE + .get_val::>(cache::CacheKey { + key: key.to_string(), + prefix: state.tenant.redis_key_prefix.clone(), + }) + .await +} + +/// Refreshes the cached elimination routing configs specific to tenant and profile +#[cfg(feature = "v1")] +pub async fn refresh_elimination_routing_cache( + state: &SessionState, + key: &str, + elimination_routing_config: routing_types::EliminationRoutingConfig, +) -> Arc { + let config = Arc::new(elimination_routing_config); + cache::ELIMINATION_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 elimination based routing configs +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn fetch_elimintaion_routing_configs( + state: &SessionState, + business_profile: &domain::Profile, + elimination_routing_id: id_type::RoutingId, +) -> RouterResult { + let key = format!( + "{}_{}", + business_profile.get_id().get_string_repr(), + elimination_routing_id.get_string_repr() + ); + + if let Some(config) = + get_cached_elimination_routing_config_for_profile(state, key.as_str()).await + { + Ok(config.as_ref().clone()) + } else { + let elimination_algorithm = state + .store + .find_routing_algorithm_by_profile_id_algorithm_id( + business_profile.get_id(), + &elimination_routing_id, + ) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable( + "unable to retrieve elimination routing algorithm for profile from db", + )?; + + let elimination_config = elimination_algorithm + .algorithm_data + .parse_value::("EliminationRoutingConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse EliminationRoutingConfig struct")?; + + refresh_elimination_routing_cache(state, key.as_str(), elimination_config.clone()).await; + + Ok(elimination_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( +pub async fn fetch_success_based_routing_config( state: &SessionState, business_profile: &domain::Profile, success_based_routing_id: id_type::RoutingId, @@ -621,7 +701,7 @@ pub async fn fetch_success_based_routing_configs( { Ok(config.as_ref().clone()) } else { - let success_rate_algorithm = state + let dynamic_algorithm = state .store .find_routing_algorithm_by_profile_id_algorithm_id( business_profile.get_id(), @@ -629,17 +709,138 @@ pub async fn fetch_success_based_routing_configs( ) .await .change_context(errors::ApiErrorResponse::ResourceIdNotFound) - .attach_printable("unable to retrieve success_rate_algorithm for profile from db")?; + .attach_printable("unable to retrieve dynamic algorithm for profile from db")?; - let success_rate_config = success_rate_algorithm + let success_based_routing_config = dynamic_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; + refresh_success_based_routing_cache( + state, + key.as_str(), + success_based_routing_config.clone(), + ) + .await; - Ok(success_rate_config) + Ok(success_based_routing_config) + } +} + +/// update window for elimination based dynamic routing +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn update_window_for_elimination_routing( + state: &SessionState, + payment_attempt: &storage::PaymentAttempt, + business_profile: &domain::Profile, + elimination_routing_configs_params_interpolator: DynamicRoutingConfigParamsInterpolator, + gsm_error_category: common_enums::ErrorCategory, +) -> RouterResult<()> { + let elimination_based_dynamic_routing_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 elimination_algo_ref = elimination_based_dynamic_routing_ref + .elimination_routing_algorithm + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("elimination_based_routing_algorithm not found in dynamic_routing_algorithm from business_profile table")?; + + if elimination_algo_ref.enabled_feature != routing_types::DynamicRoutingFeatures::None { + let client = state + .grpc_client + .dynamic_routing + .elimination_rate_client + .as_ref() + .ok_or(errors::ApiErrorResponse::GenericNotFoundError { + message: "elimination_rate gRPC client not found".to_string(), + })?; + + let elimination_routing_config = fetch_elimintaion_routing_configs( + state, + business_profile, + elimination_algo_ref + .algorithm_id_with_timestamp + .algorithm_id + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "elimination_routing_algorithm_id not found in business_profile", + )?, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to retrieve elimination based dynamic routing configs")?; + + let payment_connector = &payment_attempt.connector.clone().ok_or( + errors::ApiErrorResponse::GenericNotFoundError { + message: "unable to derive payment connector from payment attempt".to_string(), + }, + )?; + + let elimination_routing_configs = fetch_elimintaion_routing_configs( + state, + business_profile, + elimination_algo_ref + .algorithm_id_with_timestamp + .algorithm_id + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "elimination_routing_algorithm_id not found in business_profile", + )?, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to retrieve elimination 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 elimination_routing_config_params = elimination_routing_configs_params_interpolator + .get_string_val( + elimination_routing_configs + .params + .as_ref() + .ok_or(errors::RoutingError::EliminationBasedRoutingParamsNotFoundError) + .change_context(errors::ApiErrorResponse::InternalServerError)?, + ); + + client + .update_elimination_bucket_config( + tenant_business_profile_id, + elimination_routing_config_params, + vec![routing_types::RoutableConnectorChoiceWithBucketName::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(), + }, + gsm_error_category.to_string(), + )], + elimination_routing_config.elimination_analyser_config, + state.get_grpc_headers(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to update success based routing window in dynamic routing service", + )?; + Ok(()) + } else { + Ok(()) } } @@ -651,7 +852,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( payment_attempt: &storage::PaymentAttempt, routable_connectors: Vec, business_profile: &domain::Profile, - success_based_routing_config_params_interpolator: SuccessBasedRoutingConfigParamsInterpolator, + success_based_routing_config_params_interpolator: DynamicRoutingConfigParamsInterpolator, ) -> RouterResult<()> { let success_based_dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef = business_profile @@ -684,7 +885,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( }, )?; - let success_based_routing_configs = fetch_success_based_routing_configs( + let success_based_routing_configs = fetch_success_based_routing_config( state, business_profile, success_based_algo_ref @@ -1270,7 +1471,8 @@ pub async fn default_specific_dynamic_routing_setup( Ok(ApplicationResponse::Json(new_record)) } -pub struct SuccessBasedRoutingConfigParamsInterpolator { +#[derive(Clone, Debug)] +pub struct DynamicRoutingConfigParamsInterpolator { pub payment_method: Option, pub payment_method_type: Option, pub authentication_type: Option, @@ -1280,7 +1482,7 @@ pub struct SuccessBasedRoutingConfigParamsInterpolator { pub card_bin: Option, } -impl SuccessBasedRoutingConfigParamsInterpolator { +impl DynamicRoutingConfigParamsInterpolator { pub fn new( payment_method: Option, payment_method_type: Option,