feat(dynamic_routing): Decision engine config API integration (#8044)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sarthak Soni
2025-05-22 23:42:40 +05:30
committed by GitHub
parent d1fe2841c1
commit d41f65381a
9 changed files with 889 additions and 100 deletions

View File

@ -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<f64>,
pub default_bucket_size: Option<i32>,
pub default_hedging_percent: Option<f64>,
pub default_lower_reset_factor: Option<f64>,
pub default_upper_reset_factor: Option<f64>,
pub default_gateway_extra_score: Option<Vec<DecisionEngineGatewayWiseExtraScore>>,
pub sub_level_input_config: Option<Vec<DecisionEngineSRSubLevelInputConfig>>,
}
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<String>,
pub payment_method: Option<String>,
pub latency_threshold: Option<f64>,
pub bucket_size: Option<i32>,
pub hedging_percent: Option<f64>,
pub lower_reset_factor: Option<f64>,
pub upper_reset_factor: Option<f64>,
pub gateway_extra_score: Option<Vec<DecisionEngineGatewayWiseExtraScore>>,
}
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<String>,
}
#[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,
}

View File

@ -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<Vec<DynamicRoutingConfigParams>>,
pub elimination_analyser_config: Option<EliminationAnalyserConfig>,
#[schema(value_type = DecisionEngineEliminationData)]
pub decision_engine_configs: Option<open_router::DecisionEngineEliminationData>,
}
#[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<open_router::DecisionEngineEliminationData, error_stack::Report<ValidationError>>
{
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<Vec<DynamicRoutingConfigParams>>,
pub config: Option<SuccessBasedRoutingConfigBody>,
#[schema(value_type = DecisionEngineSuccessRateData)]
pub decision_engine_configs: Option<open_router::DecisionEngineSuccessRateData>,
}
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<open_router::DecisionEngineSuccessRateData, error_stack::Report<ValidationError>>
{
self.decision_engine_configs
.clone()
.ok_or(error_stack::Report::new(
ValidationError::MissingRequiredField {
field_name: "decision_engine_configs".to_string(),
},
))
}
}