diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d7f3cd4180..3ab7d8d45c 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -11706,6 +11706,123 @@ } } }, + "DecisionEngineEliminationData": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "type": "number", + "format": "double" + } + } + }, + "DecisionEngineGatewayWiseExtraScore": { + "type": "object", + "required": [ + "gatewayName", + "gatewaySigmaFactor" + ], + "properties": { + "gatewayName": { + "type": "string" + }, + "gatewaySigmaFactor": { + "type": "number", + "format": "double" + } + } + }, + "DecisionEngineSRSubLevelInputConfig": { + "type": "object", + "properties": { + "paymentMethodType": { + "type": "string", + "nullable": true + }, + "paymentMethod": { + "type": "string", + "nullable": true + }, + "latencyThreshold": { + "type": "number", + "format": "double", + "nullable": true + }, + "bucketSize": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "hedgingPercent": { + "type": "number", + "format": "double", + "nullable": true + }, + "lowerResetFactor": { + "type": "number", + "format": "double", + "nullable": true + }, + "upperResetFactor": { + "type": "number", + "format": "double", + "nullable": true + }, + "gatewayExtraScore": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DecisionEngineGatewayWiseExtraScore" + }, + "nullable": true + } + } + }, + "DecisionEngineSuccessRateData": { + "type": "object", + "properties": { + "defaultLatencyThreshold": { + "type": "number", + "format": "double", + "nullable": true + }, + "defaultBucketSize": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "defaultHedgingPercent": { + "type": "number", + "format": "double", + "nullable": true + }, + "defaultLowerResetFactor": { + "type": "number", + "format": "double", + "nullable": true + }, + "defaultUpperResetFactor": { + "type": "number", + "format": "double", + "nullable": true + }, + "defaultGatewayExtraScore": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DecisionEngineGatewayWiseExtraScore" + }, + "nullable": true + }, + "subLevelInputConfig": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DecisionEngineSRSubLevelInputConfig" + }, + "nullable": true + } + } + }, "DecoupledAuthenticationType": { "type": "string", "enum": [ @@ -27140,6 +27257,9 @@ }, "SuccessBasedRoutingConfig": { "type": "object", + "required": [ + "decision_engine_configs" + ], "properties": { "params": { "type": "array", @@ -27155,6 +27275,9 @@ } ], "nullable": true + }, + "decision_engine_configs": { + "$ref": "#/components/schemas/DecisionEngineSuccessRateData" } } }, diff --git a/crates/api_models/src/open_router.rs b/crates/api_models/src/open_router.rs index f80623efc6..f15d9cbcfe 100644 --- a/crates/api_models/src/open_router.rs +++ b/crates/api_models/src/open_router.rs @@ -9,6 +9,7 @@ pub use euclid::{ }, }; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use crate::{ enums::{Currency, PaymentMethod}, @@ -172,3 +173,148 @@ pub enum TxnStatus { Failure, Declined, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DecisionEngineConfigSetupRequest { + pub merchant_id: String, + pub config: DecisionEngineConfigVariant, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type", content = "data")] +#[serde(rename_all = "camelCase")] +pub enum DecisionEngineConfigVariant { + SuccessRate(DecisionEngineSuccessRateData), + Elimination(DecisionEngineEliminationData), +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DecisionEngineSuccessRateData { + pub default_latency_threshold: Option, + pub default_bucket_size: Option, + pub default_hedging_percent: Option, + pub default_lower_reset_factor: Option, + pub default_upper_reset_factor: Option, + pub default_gateway_extra_score: Option>, + pub sub_level_input_config: Option>, +} + +impl DecisionEngineSuccessRateData { + pub fn update(&mut self, new_config: Self) { + if let Some(threshold) = new_config.default_latency_threshold { + self.default_latency_threshold = Some(threshold); + } + if let Some(bucket_size) = new_config.default_bucket_size { + self.default_bucket_size = Some(bucket_size); + } + if let Some(hedging_percent) = new_config.default_hedging_percent { + self.default_hedging_percent = Some(hedging_percent); + } + if let Some(lower_reset_factor) = new_config.default_lower_reset_factor { + self.default_lower_reset_factor = Some(lower_reset_factor); + } + if let Some(upper_reset_factor) = new_config.default_upper_reset_factor { + self.default_upper_reset_factor = Some(upper_reset_factor); + } + if let Some(gateway_extra_score) = new_config.default_gateway_extra_score { + self.default_gateway_extra_score + .as_mut() + .map(|score| score.extend(gateway_extra_score)); + } + if let Some(sub_level_input_config) = new_config.sub_level_input_config { + self.sub_level_input_config.as_mut().map(|config| { + config.extend(sub_level_input_config); + }); + } + } +} +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DecisionEngineSRSubLevelInputConfig { + pub payment_method_type: Option, + pub payment_method: Option, + pub latency_threshold: Option, + pub bucket_size: Option, + pub hedging_percent: Option, + pub lower_reset_factor: Option, + pub upper_reset_factor: Option, + pub gateway_extra_score: Option>, +} + +impl DecisionEngineSRSubLevelInputConfig { + pub fn update(&mut self, new_config: Self) { + if let Some(payment_method_type) = new_config.payment_method_type { + self.payment_method_type = Some(payment_method_type); + } + if let Some(payment_method) = new_config.payment_method { + self.payment_method = Some(payment_method); + } + if let Some(latency_threshold) = new_config.latency_threshold { + self.latency_threshold = Some(latency_threshold); + } + if let Some(bucket_size) = new_config.bucket_size { + self.bucket_size = Some(bucket_size); + } + if let Some(hedging_percent) = new_config.hedging_percent { + self.hedging_percent = Some(hedging_percent); + } + if let Some(lower_reset_factor) = new_config.lower_reset_factor { + self.lower_reset_factor = Some(lower_reset_factor); + } + if let Some(upper_reset_factor) = new_config.upper_reset_factor { + self.upper_reset_factor = Some(upper_reset_factor); + } + if let Some(gateway_extra_score) = new_config.gateway_extra_score { + self.gateway_extra_score + .as_mut() + .map(|score| score.extend(gateway_extra_score)); + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DecisionEngineGatewayWiseExtraScore { + pub gateway_name: String, + pub gateway_sigma_factor: f64, +} + +impl DecisionEngineGatewayWiseExtraScore { + pub fn update(&mut self, new_config: Self) { + self.gateway_name = new_config.gateway_name; + self.gateway_sigma_factor = new_config.gateway_sigma_factor; + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DecisionEngineEliminationData { + pub threshold: f64, +} + +impl DecisionEngineEliminationData { + pub fn update(&mut self, new_config: Self) { + self.threshold = new_config.threshold; + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MerchantAccount { + pub merchant_id: String, + pub gateway_success_rate_based_decider_input: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FetchRoutingConfig { + pub merchant_id: String, + pub algorithm: AlgorithmType, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub enum AlgorithmType { + SuccessRate, + Elimination, + DebitRouting, +} diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index e1802ab56f..12075935cd 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -1,6 +1,10 @@ use std::fmt::Debug; -use common_utils::{errors::ParsingError, ext_traits::ValueExt, pii}; +use common_utils::{ + errors::{ParsingError, ValidationError}, + ext_traits::ValueExt, + pii, +}; pub use euclid::{ dssa::types::EuclidAnalysable, frontend::{ @@ -11,7 +15,17 @@ pub use euclid::{ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::enums::{RoutableConnectors, TransactionType}; +use crate::{ + enums::{RoutableConnectors, TransactionType}, + open_router, +}; + +// Define constants for default values +const DEFAULT_LATENCY_THRESHOLD: f64 = 90.0; +const DEFAULT_BUCKET_SIZE: i32 = 200; +const DEFAULT_HEDGING_PERCENT: f64 = 5.0; +const DEFAULT_ELIMINATION_THRESHOLD: f64 = 0.35; +const DEFAULT_PAYMENT_METHOD: &str = "CARD"; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] @@ -834,6 +848,8 @@ pub struct ToggleDynamicRoutingPath { pub struct EliminationRoutingConfig { pub params: Option>, pub elimination_analyser_config: Option, + #[schema(value_type = DecisionEngineEliminationData)] + pub decision_engine_configs: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, ToSchema)] @@ -861,6 +877,7 @@ impl Default for EliminationRoutingConfig { bucket_size: Some(5), bucket_leak_interval_in_secs: Some(60), }), + decision_engine_configs: None, } } } @@ -875,6 +892,34 @@ impl EliminationRoutingConfig { .as_mut() .map(|config| config.update(new_config)); } + if let Some(new_config) = new.decision_engine_configs { + self.decision_engine_configs + .as_mut() + .map(|config| config.update(new_config)); + } + } + + pub fn open_router_config_default() -> Self { + Self { + elimination_analyser_config: None, + params: None, + decision_engine_configs: Some(open_router::DecisionEngineEliminationData { + threshold: DEFAULT_ELIMINATION_THRESHOLD, + }), + } + } + + pub fn get_decision_engine_configs( + &self, + ) -> Result> + { + self.decision_engine_configs + .clone() + .ok_or(error_stack::Report::new( + ValidationError::MissingRequiredField { + field_name: "decision_engine_configs".to_string(), + }, + )) } } @@ -882,6 +927,8 @@ impl EliminationRoutingConfig { pub struct SuccessBasedRoutingConfig { pub params: Option>, pub config: Option, + #[schema(value_type = DecisionEngineSuccessRateData)] + pub decision_engine_configs: Option, } impl Default for SuccessBasedRoutingConfig { @@ -898,6 +945,7 @@ impl Default for SuccessBasedRoutingConfig { }), specificity_level: SuccessRateSpecificityLevel::default(), }), + decision_engine_configs: None, } } } @@ -982,6 +1030,51 @@ impl SuccessBasedRoutingConfig { if let Some(new_config) = new.config { self.config.as_mut().map(|config| config.update(new_config)); } + if let Some(new_config) = new.decision_engine_configs { + self.decision_engine_configs + .as_mut() + .map(|config| config.update(new_config)); + } + } + + pub fn open_router_config_default() -> Self { + Self { + params: None, + config: None, + decision_engine_configs: Some(open_router::DecisionEngineSuccessRateData { + default_latency_threshold: Some(DEFAULT_LATENCY_THRESHOLD), + default_bucket_size: Some(DEFAULT_BUCKET_SIZE), + default_hedging_percent: Some(DEFAULT_HEDGING_PERCENT), + default_lower_reset_factor: None, + default_upper_reset_factor: None, + default_gateway_extra_score: None, + sub_level_input_config: Some(vec![ + open_router::DecisionEngineSRSubLevelInputConfig { + payment_method_type: Some(DEFAULT_PAYMENT_METHOD.to_string()), + payment_method: None, + latency_threshold: None, + bucket_size: Some(DEFAULT_BUCKET_SIZE), + hedging_percent: Some(DEFAULT_HEDGING_PERCENT), + lower_reset_factor: None, + upper_reset_factor: None, + gateway_extra_score: None, + }, + ]), + }), + } + } + + pub fn get_decision_engine_configs( + &self, + ) -> Result> + { + self.decision_engine_configs + .clone() + .ok_or(error_stack::Report::new( + ValidationError::MissingRequiredField { + field_name: "decision_engine_configs".to_string(), + }, + )) } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 79c4467f03..b042d2bdbd 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -743,6 +743,10 @@ Never share your secret api keys. Keep them guarded and secure. api_models::feature_matrix::PaymentMethodSpecificFeatures, api_models::feature_matrix::CardSpecificFeatures, api_models::feature_matrix::SupportedPaymentMethod, + api_models::open_router::DecisionEngineSuccessRateData, + api_models::open_router::DecisionEngineGatewayWiseExtraScore, + api_models::open_router::DecisionEngineSRSubLevelInputConfig, + api_models::open_router::DecisionEngineEliminationData, )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 5db9e708a4..860d82ab48 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -258,6 +258,26 @@ pub async fn create_merchant_account( .await .to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantAccount)?; + // Call to DE here + // Check if creation should be based on default profile + #[cfg(all(feature = "dynamic_routing", feature = "v1"))] + { + if state.conf.open_router.enabled { + merchant_account + .default_profile + .as_ref() + .async_map(|profile_id| { + routing::helpers::create_decision_engine_merchant(&state, profile_id) + }) + .await + .transpose() + .map_err(|err| { + crate::logger::error!("Failed to create merchant in Decision Engine {err:?}"); + }) + .ok(); + } + } + let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(domain::Context( merchant_account.clone(), key_store.clone(), @@ -1171,6 +1191,25 @@ pub async fn merchant_account_delete( is_deleted = is_merchant_account_deleted && is_merchant_key_store_deleted; } + // Call to DE here + #[cfg(all(feature = "dynamic_routing", feature = "v1"))] + { + if state.conf.open_router.enabled && is_deleted { + merchant_account + .default_profile + .as_ref() + .async_map(|profile_id| { + routing::helpers::delete_decision_engine_merchant(&state, profile_id) + }) + .await + .transpose() + .map_err(|err| { + crate::logger::error!("Failed to delete merchant in Decision Engine {err:?}"); + }) + .ok(); + } + } + let state = state.clone(); authentication::decision::spawn_tracked_job( async move { diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 83d3abef44..2b851c192b 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -59,7 +59,9 @@ use crate::core::routing::transformers::OpenRouterDecideGatewayRequestExt; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use crate::headers; use crate::{ - core::{errors, errors as oss_errors, payments::routing::utils::EuclidApiHandler, routing}, + core::{ + errors, errors as oss_errors, payments::routing::utils::DecisionEngineApiHandler, routing, + }, logger, services, types::{ api::{self, routing as routing_types}, @@ -1605,14 +1607,15 @@ pub async fn perform_open_routing_for_debit_routing( Some(or_types::RankingAlgorithm::NtwBasedRouting), ); - let response: RoutingResult = utils::EuclidApiClient::send_euclid_request( - state, - services::Method::Post, - "decide-gateway", - Some(open_router_req_body), - None, - ) - .await; + let response: RoutingResult = + utils::EuclidApiClient::send_decision_engine_request( + state, + services::Method::Post, + "decide-gateway", + Some(open_router_req_body), + None, + ) + .await; let output = match response { Ok(decided_gateway) => { diff --git a/crates/router/src/core/payments/routing/utils.rs b/crates/router/src/core/payments/routing/utils.rs index 551694a0aa..e82ab1d8be 100644 --- a/crates/router/src/core/payments/routing/utils.rs +++ b/crates/router/src/core/payments/routing/utils.rs @@ -18,8 +18,8 @@ use crate::{ // New Trait for handling Euclid API calls #[async_trait] -pub trait EuclidApiHandler { - async fn send_euclid_request( +pub trait DecisionEngineApiHandler { + async fn send_decision_engine_request( state: &SessionState, http_method: services::Method, path: &str, @@ -30,7 +30,7 @@ pub trait EuclidApiHandler { Req: Serialize + Send + Sync + 'static, Res: serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug; - async fn send_euclid_request_without_response_parsing( + async fn send_decision_engine_request_without_response_parsing( state: &SessionState, http_method: services::Method, path: &str, @@ -41,54 +41,54 @@ pub trait EuclidApiHandler { Req: Serialize + Send + Sync + 'static; } -// Struct to implement the EuclidApiHandler trait +// Struct to implement the DecisionEngineApiHandler trait pub struct EuclidApiClient; -impl EuclidApiClient { - async fn build_and_send_euclid_http_request( - state: &SessionState, - http_method: services::Method, - path: &str, - request_body: Option, - timeout: Option, - context_message: &str, - ) -> RoutingResult - where - Req: Serialize + Send + Sync + 'static, - { - let euclid_base_url = &state.conf.open_router.url; - let url = format!("{}/{}", euclid_base_url, path); - logger::debug!(euclid_api_call_url = %url, euclid_request_path = %path, http_method = ?http_method, "decision_engine_euclid: Initiating Euclid API call ({})", context_message); +pub struct ConfigApiClient; - let mut request_builder = services::RequestBuilder::new() - .method(http_method) - .url(&url); +pub async fn build_and_send_decision_engine_http_request( + state: &SessionState, + http_method: services::Method, + path: &str, + request_body: Option, + timeout: Option, + context_message: &str, +) -> RoutingResult +where + Req: Serialize + Send + Sync + 'static, +{ + let decision_engine_base_url = &state.conf.open_router.url; + let url = format!("{}/{}", decision_engine_base_url, path); + logger::debug!(decision_engine_api_call_url = %url, decision_engine_request_path = %path, http_method = ?http_method, "decision_engine: Initiating decision_engine API call ({})", context_message); - if let Some(body_content) = request_body { - let body = common_utils::request::RequestContent::Json(Box::new(body_content)); - request_builder = request_builder.set_body(body); - } + let mut request_builder = services::RequestBuilder::new() + .method(http_method) + .url(&url); - let http_request = request_builder.build(); - logger::info!(?http_request, euclid_request_path = %path, "decision_engine_euclid: Constructed Euclid API request details ({})", context_message); - - state - .api_client - .send_request(state, http_request, timeout, false) - .await - .change_context(errors::RoutingError::DslExecutionError) - .attach_printable_lazy(|| { - format!( - "Euclid API call to path '{}' unresponsive ({})", - path, context_message - ) - }) + if let Some(body_content) = request_body { + let body = common_utils::request::RequestContent::Json(Box::new(body_content)); + request_builder = request_builder.set_body(body); } + + let http_request = request_builder.build(); + logger::info!(?http_request, decision_engine_request_path = %path, "decision_engine: Constructed Decision Engine API request details ({})", context_message); + + state + .api_client + .send_request(state, http_request, timeout, false) + .await + .change_context(errors::RoutingError::DslExecutionError) + .attach_printable_lazy(|| { + format!( + "Decision Engine API call to path '{}' unresponsive ({})", + path, context_message + ) + }) } #[async_trait] -impl EuclidApiHandler for EuclidApiClient { - async fn send_euclid_request( +impl DecisionEngineApiHandler for EuclidApiClient { + async fn send_decision_engine_request( state: &SessionState, http_method: services::Method, path: &str, @@ -99,7 +99,7 @@ impl EuclidApiHandler for EuclidApiClient { Req: Serialize + Send + Sync + 'static, Res: serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug, { - let response = Self::build_and_send_euclid_http_request( + let response = build_and_send_decision_engine_http_request( state, http_method, path, @@ -128,7 +128,7 @@ impl EuclidApiHandler for EuclidApiClient { Ok(parsed_response) } - async fn send_euclid_request_without_response_parsing( + async fn send_decision_engine_request_without_response_parsing( state: &SessionState, http_method: services::Method, path: &str, @@ -138,7 +138,7 @@ impl EuclidApiHandler for EuclidApiClient { where Req: Serialize + Send + Sync + 'static, { - let response = Self::build_and_send_euclid_http_request( + let response = build_and_send_decision_engine_http_request( state, http_method, path, @@ -153,6 +153,73 @@ impl EuclidApiHandler for EuclidApiClient { } } +#[async_trait] +impl DecisionEngineApiHandler for ConfigApiClient { + async fn send_decision_engine_request( + state: &SessionState, + http_method: services::Method, + path: &str, + request_body: Option, + timeout: Option, + ) -> RoutingResult + where + Req: Serialize + Send + Sync + 'static, + Res: serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug, + { + let response = build_and_send_decision_engine_http_request( + state, + http_method, + path, + request_body, + timeout, + "parsing response", + ) + .await?; + logger::debug!(decision_engine_config_response = ?response, decision_engine_request_path = %path, "decision_engine_config: Received raw response from Decision Engine config API"); + + let parsed_response = response + .json::() + .await + .change_context(errors::RoutingError::GenericConversionError { + from: "ApiResponse".to_string(), + to: std::any::type_name::().to_string(), + }) + .attach_printable_lazy(|| { + format!( + "Unable to parse response of type '{}' received from Decision Engine config API path: {}", + std::any::type_name::(), + path + ) + })?; + logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::(), decision_engine_request_path = %path, "decision_engine_config: Successfully parsed response from Decision Engine config API"); + Ok(parsed_response) + } + + async fn send_decision_engine_request_without_response_parsing( + state: &SessionState, + http_method: services::Method, + path: &str, + request_body: Option, + timeout: Option, + ) -> RoutingResult<()> + where + Req: Serialize + Send + Sync + 'static, + { + let response = build_and_send_decision_engine_http_request( + state, + http_method, + path, + request_body, + timeout, + "not parsing response", + ) + .await?; + + logger::debug!(decision_engine_response = ?response, decision_engine_request_path = %path, "decision_engine_config: Received raw response from Decision Engine config API"); + Ok(()) + } +} + const EUCLID_API_TIMEOUT: u64 = 5; pub async fn perform_decision_euclid_routing( @@ -164,7 +231,7 @@ pub async fn perform_decision_euclid_routing( let routing_request = convert_backend_input_to_routing_eval(created_by, input)?; - let euclid_response: RoutingEvaluateResponse = EuclidApiClient::send_euclid_request( + let euclid_response: RoutingEvaluateResponse = EuclidApiClient::send_decision_engine_request( state, services::Method::Post, "routing/evaluate", @@ -186,7 +253,7 @@ pub async fn create_de_euclid_routing_algo( logger::debug!("decision_engine_euclid: create api call for euclid routing rule creation"); logger::debug!(decision_engine_euclid_request=?routing_request,"decision_engine_euclid"); - let euclid_response: RoutingDictionaryRecord = EuclidApiClient::send_euclid_request( + let euclid_response: RoutingDictionaryRecord = EuclidApiClient::send_decision_engine_request( state, services::Method::Post, "routing/create", @@ -205,7 +272,7 @@ pub async fn link_de_euclid_routing_algorithm( ) -> RoutingResult<()> { logger::debug!("decision_engine_euclid: link api call for euclid routing algorithm"); - EuclidApiClient::send_euclid_request_without_response_parsing( + EuclidApiClient::send_decision_engine_request_without_response_parsing( state, services::Method::Post, "routing/activate", @@ -224,7 +291,7 @@ pub async fn list_de_euclid_routing_algorithms( ) -> RoutingResult> { logger::debug!("decision_engine_euclid: list api call for euclid routing algorithms"); let created_by = routing_list_request.created_by; - let response: Vec = EuclidApiClient::send_euclid_request( + let response: Vec = EuclidApiClient::send_decision_engine_request( state, services::Method::Post, format!("routing/list/{created_by}").as_str(), diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 6b86bb83f5..893c66af24 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -19,6 +19,8 @@ use external_services::grpc_client::dynamic_routing::{ elimination_based_client::EliminationBasedRouting, success_rate_client::SuccessBasedDynamicRouting, }; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use helpers::update_decision_engine_dynamic_routing_setup; use hyperswitch_domain_models::{mandates, payment_address}; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use router_env::logger; @@ -564,6 +566,24 @@ pub async fn link_routing_config( .enabled_feature, routing_types::DynamicRoutingType::SuccessRateBasedRouting, ); + + // Call to DE here to update SR configs + #[cfg(all(feature = "dynamic_routing", feature = "v1"))] + { + if state.conf.open_router.enabled { + update_decision_engine_dynamic_routing_setup( + &state, + business_profile.get_id(), + routing_algorithm.algorithm_data.clone(), + routing_types::DynamicRoutingType::SuccessRateBasedRouting, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to update the success rate routing config in Decision Engine", + )?; + } + } } else if routing_algorithm.name == helpers::ELIMINATION_BASED_DYNAMIC_ROUTING_ALGORITHM { dynamic_routing_ref.update_algorithm_id( @@ -578,6 +598,22 @@ pub async fn link_routing_config( .enabled_feature, routing_types::DynamicRoutingType::EliminationRouting, ); + #[cfg(all(feature = "dynamic_routing", feature = "v1"))] + { + if state.conf.open_router.enabled { + update_decision_engine_dynamic_routing_setup( + &state, + business_profile.get_id(), + routing_algorithm.algorithm_data.clone(), + routing_types::DynamicRoutingType::EliminationRouting, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to update the elimination routing config in Decision Engine", + )?; + } + } } else if routing_algorithm.name == helpers::CONTRACT_BASED_DYNAMIC_ROUTING_ALGORITHM { dynamic_routing_ref.update_algorithm_id( algorithm_id, @@ -1516,7 +1552,7 @@ pub async fn success_based_routing_update_configs( 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), + algorithm_data: serde_json::json!(config_to_update.clone()), created_at: timestamp, modified_at: timestamp, algorithm_for: dynamic_routing_algo_to_update.algorithm_for, @@ -1551,23 +1587,25 @@ pub async fn success_based_routing_update_configs( router_env::metric_attributes!(("profile_id", profile_id.clone())), ); - state - .grpc_client - .dynamic_routing - .success_rate_client - .as_ref() - .async_map(|sr_client| async { - sr_client - .invalidate_success_rate_routing_keys( - profile_id.get_string_repr().into(), - state.get_grpc_headers(), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to invalidate the routing keys") - }) - .await - .transpose()?; + if !state.conf.open_router.enabled { + state + .grpc_client + .dynamic_routing + .success_rate_client + .as_ref() + .async_map(|sr_client| async { + sr_client + .invalidate_success_rate_routing_keys( + profile_id.get_string_repr().into(), + state.get_grpc_headers(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to invalidate the routing keys") + }) + .await + .transpose()?; + } Ok(service_api::ApplicationResponse::Json(new_record)) } @@ -1653,23 +1691,25 @@ pub async fn elimination_routing_update_configs( router_env::metric_attributes!(("profile_id", profile_id.clone())), ); - state - .grpc_client - .dynamic_routing - .elimination_based_client - .as_ref() - .async_map(|er_client| async { - er_client - .invalidate_elimination_bucket( - profile_id.get_string_repr().into(), - state.get_grpc_headers(), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to invalidate the elimination routing keys") - }) - .await - .transpose()?; + if !state.conf.open_router.enabled { + state + .grpc_client + .dynamic_routing + .elimination_based_client + .as_ref() + .async_map(|er_client| async { + er_client + .invalidate_elimination_bucket( + profile_id.get_string_repr().into(), + state.get_grpc_headers(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to invalidate the elimination routing keys") + }) + .await + .transpose()?; + } Ok(service_api::ApplicationResponse::Json(new_record)) } diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index e5cbd1b653..1a092c8cf6 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -52,7 +52,12 @@ use crate::{ }; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] use crate::{ - core::{metrics as core_metrics, routing}, + core::{ + metrics as core_metrics, + payments::routing::utils::{self as routing_utils, DecisionEngineApiHandler}, + routing, + }, + services, types::transformers::ForeignInto, }; pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str = @@ -62,6 +67,12 @@ pub const ELIMINATION_BASED_DYNAMIC_ROUTING_ALGORITHM: &str = pub const CONTRACT_BASED_DYNAMIC_ROUTING_ALGORITHM: &str = "Contract based dynamic routing algorithm"; +pub const DECISION_ENGINE_RULE_CREATE_ENDPOINT: &str = "rule/create"; +pub const DECISION_ENGINE_RULE_UPDATE_ENDPOINT: &str = "rule/update"; +pub const DECISION_ENGINE_RULE_DELETE_ENDPOINT: &str = "rule/delete"; +pub const DECISION_ENGINE_MERCHANT_BASE_ENDPOINT: &str = "merchant-account"; +pub const DECISION_ENGINE_MERCHANT_CREATE_ENDPOINT: &str = "merchant-account/create"; + /// Provides us with all the configured configs of the Merchant in the ascending time configured /// manner and chooses the first of them pub async fn get_merchant_default_config( @@ -1625,6 +1636,18 @@ pub async fn disable_dynamic_routing_algorithm( } }; + // Call to DE here + if state.conf.open_router.enabled { + disable_decision_engine_dynamic_routing_setup( + state, + business_profile.get_id(), + dynamic_routing_type, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to disable dynamic routing setup in decision engine")?; + } + // redact cache for dynamic routing config let _ = cache::redact_from_redis_and_publish( state.store.get_cache_store().as_ref(), @@ -1801,8 +1824,12 @@ pub async fn default_specific_dynamic_routing_setup( let timestamp = common_utils::date_time::now(); let algo = match dynamic_routing_type { routing_types::DynamicRoutingType::SuccessRateBasedRouting => { - let default_success_based_routing_config = - routing_types::SuccessBasedRoutingConfig::default(); + let default_success_based_routing_config = if state.conf.open_router.enabled { + routing_types::SuccessBasedRoutingConfig::open_router_config_default() + } else { + routing_types::SuccessBasedRoutingConfig::default() + }; + routing_algorithm::RoutingAlgorithm { algorithm_id: algorithm_id.clone(), profile_id: profile_id.clone(), @@ -1818,8 +1845,11 @@ pub async fn default_specific_dynamic_routing_setup( } } routing_types::DynamicRoutingType::EliminationRouting => { - let default_elimination_routing_config = - routing_types::EliminationRoutingConfig::default(); + let default_elimination_routing_config = if state.conf.open_router.enabled { + routing_types::EliminationRoutingConfig::open_router_config_default() + } else { + routing_types::EliminationRoutingConfig::default() + }; routing_algorithm::RoutingAlgorithm { algorithm_id: algorithm_id.clone(), profile_id: profile_id.clone(), @@ -1843,6 +1873,19 @@ pub async fn default_specific_dynamic_routing_setup( } }; + // Call to DE here + // Need to map out the cases if this call should always be made or not + if state.conf.open_router.enabled { + enable_decision_engine_dynamic_routing_setup( + state, + business_profile.get_id(), + dynamic_routing_type, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to setup decision engine dynamic routing")?; + } + let record = db .insert_routing_algorithm(algo) .await @@ -1945,3 +1988,234 @@ impl DynamicRoutingConfigParamsInterpolator { parts.join(":") } } + +#[cfg(all(feature = "dynamic_routing", feature = "v1"))] +#[instrument(skip_all)] +pub async fn enable_decision_engine_dynamic_routing_setup( + state: &SessionState, + profile_id: &id_type::ProfileId, + dynamic_routing_type: routing_types::DynamicRoutingType, +) -> RouterResult<()> { + logger::debug!( + "performing call with open_router for profile {}", + profile_id.get_string_repr() + ); + + let default_engine_config_request = match dynamic_routing_type { + routing_types::DynamicRoutingType::SuccessRateBasedRouting => { + let default_success_based_routing_config = + routing_types::SuccessBasedRoutingConfig::open_router_config_default(); + open_router::DecisionEngineConfigSetupRequest { + merchant_id: profile_id.get_string_repr().to_string(), + config: open_router::DecisionEngineConfigVariant::SuccessRate( + default_success_based_routing_config + .get_decision_engine_configs() + .change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Decision engine config not found".to_string(), + }) + .attach_printable("Decision engine config not found")?, + ), + } + } + routing_types::DynamicRoutingType::EliminationRouting => { + let default_elimination_based_routing_config = + routing_types::EliminationRoutingConfig::open_router_config_default(); + open_router::DecisionEngineConfigSetupRequest { + merchant_id: profile_id.get_string_repr().to_string(), + config: open_router::DecisionEngineConfigVariant::Elimination( + default_elimination_based_routing_config + .get_decision_engine_configs() + .change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Decision engine config not found".to_string(), + }) + .attach_printable("Decision engine config not found")?, + ), + } + } + routing_types::DynamicRoutingType::ContractBasedRouting => { + return Err((errors::ApiErrorResponse::InvalidRequestData { + message: "Contract routing cannot be set as default".to_string(), + }) + .into()) + } + }; + + routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>( + state, + services::Method::Post, + DECISION_ENGINE_RULE_CREATE_ENDPOINT, + Some(default_engine_config_request), + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to setup decision engine dynamic routing")?; + + Ok(()) +} + +#[cfg(all(feature = "dynamic_routing", feature = "v1"))] +#[instrument(skip_all)] +pub async fn update_decision_engine_dynamic_routing_setup( + state: &SessionState, + profile_id: &id_type::ProfileId, + request: serde_json::Value, + dynamic_routing_type: routing_types::DynamicRoutingType, +) -> RouterResult<()> { + logger::debug!( + "performing call with open_router for profile {}", + profile_id.get_string_repr() + ); + + let decision_engine_request = match dynamic_routing_type { + routing_types::DynamicRoutingType::SuccessRateBasedRouting => { + let success_rate_config: routing_types::SuccessBasedRoutingConfig = request + .parse_value("SuccessBasedRoutingConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize SuccessBasedRoutingConfig")?; + + open_router::DecisionEngineConfigSetupRequest { + merchant_id: profile_id.get_string_repr().to_string(), + config: open_router::DecisionEngineConfigVariant::SuccessRate( + success_rate_config + .get_decision_engine_configs() + .change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Decision engine config not found".to_string(), + }) + .attach_printable("Decision engine config not found")?, + ), + } + } + routing_types::DynamicRoutingType::EliminationRouting => { + let elimination_config: routing_types::EliminationRoutingConfig = request + .parse_value("EliminationRoutingConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize EliminationRoutingConfig")?; + + open_router::DecisionEngineConfigSetupRequest { + merchant_id: profile_id.get_string_repr().to_string(), + config: open_router::DecisionEngineConfigVariant::Elimination( + elimination_config + .get_decision_engine_configs() + .change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Decision engine config not found".to_string(), + }) + .attach_printable("Decision engine config not found")?, + ), + } + } + routing_types::DynamicRoutingType::ContractBasedRouting => { + return Err((errors::ApiErrorResponse::InvalidRequestData { + message: "Contract routing cannot be set as default".to_string(), + }) + .into()) + } + }; + + routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>( + state, + services::Method::Post, + DECISION_ENGINE_RULE_UPDATE_ENDPOINT, + Some(decision_engine_request), + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update decision engine dynamic routing")?; + + Ok(()) +} + +#[cfg(all(feature = "dynamic_routing", feature = "v1"))] +#[instrument(skip_all)] +pub async fn disable_decision_engine_dynamic_routing_setup( + state: &SessionState, + profile_id: &id_type::ProfileId, + dynamic_routing_type: routing_types::DynamicRoutingType, +) -> RouterResult<()> { + logger::debug!( + "performing call with open_router for profile {}", + profile_id.get_string_repr() + ); + + let decision_engine_request = open_router::FetchRoutingConfig { + merchant_id: profile_id.get_string_repr().to_string(), + algorithm: match dynamic_routing_type { + routing_types::DynamicRoutingType::SuccessRateBasedRouting => { + open_router::AlgorithmType::SuccessRate + } + routing_types::DynamicRoutingType::EliminationRouting => { + open_router::AlgorithmType::Elimination + } + routing_types::DynamicRoutingType::ContractBasedRouting => { + return Err((errors::ApiErrorResponse::InvalidRequestData { + message: "Contract routing is not enabled for decision engine".to_string(), + }) + .into()) + } + }, + }; + + routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>( + state, + services::Method::Post, + DECISION_ENGINE_RULE_DELETE_ENDPOINT, + Some(decision_engine_request), + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to disable decision engine dynamic routing")?; + + Ok(()) +} + +#[cfg(all(feature = "dynamic_routing", feature = "v1"))] +#[instrument(skip_all)] +pub async fn create_decision_engine_merchant( + state: &SessionState, + profile_id: &id_type::ProfileId, +) -> RouterResult<()> { + let merchant_account_req = open_router::MerchantAccount { + merchant_id: profile_id.get_string_repr().to_string(), + gateway_success_rate_based_decider_input: None, + }; + + routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>( + state, + services::Method::Post, + DECISION_ENGINE_MERCHANT_CREATE_ENDPOINT, + Some(merchant_account_req), + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to create merchant account on decision engine")?; + + Ok(()) +} + +#[cfg(all(feature = "dynamic_routing", feature = "v1"))] +#[instrument(skip_all)] +pub async fn delete_decision_engine_merchant( + state: &SessionState, + profile_id: &id_type::ProfileId, +) -> RouterResult<()> { + let path = format!( + "{}/{}", + DECISION_ENGINE_MERCHANT_BASE_ENDPOINT, + profile_id.get_string_repr() + ); + routing_utils::ConfigApiClient::send_decision_engine_request_without_response_parsing::<()>( + state, + services::Method::Delete, + &path, + None, + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to delete merchant account on decision engine")?; + + Ok(()) +}