refactor: add compatibility for decision-engine rules (#8346)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Prajjwal Kumar
2025-06-16 15:43:01 +05:30
committed by GitHub
parent 800c5e68da
commit 1ed2f210b2
3 changed files with 133 additions and 26 deletions

View File

@ -384,6 +384,8 @@ pub enum RoutingError {
OpenRouterCallFailed,
#[error("Error from open_router: {0}")]
OpenRouterError(String),
#[error("Decision engine responded with validation error: {0}")]
DecisionEngineValidationError(String),
#[error("Invalid transaction type")]
InvalidTransactionType,
}

View File

@ -111,12 +111,50 @@ impl DecisionEngineApiHandler for EuclidApiClient {
"parsing response",
)
.await?;
logger::debug!(euclid_response = ?response, euclid_request_path = %path, "decision_engine_euclid: Received raw response from Euclid API");
let parsed_response = response
.json::<Res>()
.await
.change_context(errors::RoutingError::GenericConversionError {
let status = response.status();
let response_bytes = response.bytes().await.unwrap_or_default();
let body_str = String::from_utf8_lossy(&response_bytes); // For logging
if !status.is_success() {
match serde_json::from_slice::<DeErrorResponse>(&response_bytes) {
Ok(parsed) => {
logger::error!(
decision_engine_error_code = %parsed.code,
decision_engine_error_message = %parsed.message,
decision_engine_raw_response = ?parsed.data,
"decision_engine_euclid: validation failed"
);
return Err(errors::RoutingError::DecisionEngineValidationError(
parsed.message,
)
.into());
}
Err(_) => {
logger::error!(
decision_engine_raw_response = %body_str,
"decision_engine_euclid: failed to deserialize validation error response"
);
return Err(errors::RoutingError::DecisionEngineValidationError(
"decision_engine_euclid: Failed to parse validation error from decision engine".to_string(),
)
.into());
}
}
}
logger::debug!(
euclid_response_body = %body_str,
response_status = ?status,
euclid_request_path = %path,
"decision_engine_euclid: Received raw response from Euclid API"
);
let parsed_response = serde_json::from_slice::<Res>(&response_bytes)
.map_err(|_| errors::RoutingError::GenericConversionError {
from: "ApiResponse".to_string(),
to: std::any::type_name::<Res>().to_string(),
})
@ -127,7 +165,14 @@ impl DecisionEngineApiHandler for EuclidApiClient {
path
)
})?;
logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::<Res>(), euclid_request_path = %path, "decision_engine_euclid: Successfully parsed response from Euclid API");
logger::debug!(
parsed_response = ?parsed_response,
response_type = %std::any::type_name::<Res>(),
euclid_request_path = %path,
"decision_engine_euclid: Successfully parsed response from Euclid API"
);
Ok(parsed_response)
}
@ -491,6 +536,13 @@ pub fn convert_backend_input_to_routing_eval(
})
}
#[derive(Debug, Clone, serde::Deserialize)]
struct DeErrorResponse {
code: String,
message: String,
data: Option<serde_json::Value>,
}
//TODO: temporary change will be refactored afterwards
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RoutingEvaluateRequest {
@ -530,7 +582,7 @@ pub enum ValueType {
pub type Metadata = HashMap<String, serde_json::Value>;
/// Represents a number comparison for "NumberComparisonArrayValue"
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub struct NumberComparison {
pub comparison_type: ComparisonType,
pub number: u64,
@ -583,7 +635,7 @@ pub type IfCondition = Vec<Comparison>;
/// }
/// ```
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub struct IfStatement {
// #[schema(value_type=Vec<Comparison>)]
pub condition: IfCondition,
@ -605,7 +657,7 @@ pub struct IfStatement {
/// }
/// ```
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
// #[aliases(RuleConnectorSelection = Rule<ConnectorSelection>)]
pub struct Rule {
pub name: String,
@ -625,18 +677,31 @@ pub enum RoutingType {
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub struct VolumeSplit<T> {
pub split: u8,
pub output: T,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub struct ConnectorInfo {
pub connector: String,
pub mca_id: Option<String>,
}
impl ConnectorInfo {
pub fn new(connector: String, mca_id: Option<String>) -> Self {
Self { connector, mca_id }
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Output {
Priority(Vec<String>),
VolumeSplit(Vec<VolumeSplit<String>>),
VolumeSplitPriority(Vec<VolumeSplit<Vec<String>>>),
Priority(Vec<ConnectorInfo>),
VolumeSplit(Vec<VolumeSplit<ConnectorInfo>>),
VolumeSplitPriority(Vec<VolumeSplit<Vec<ConnectorInfo>>>),
}
pub type Globals = HashMap<String, HashSet<ValueType>>;
@ -644,7 +709,7 @@ pub type Globals = HashMap<String, HashSet<ValueType>>;
/// The program, having a default connector selection and
/// a bunch of rules. Also can hold arbitrary metadata.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
// #[aliases(ProgramConnectorSelection = Program<ConnectorSelection>)]
pub struct Program {
pub globals: Globals,
@ -663,7 +728,13 @@ pub struct RoutingRule {
pub description: Option<String>,
pub metadata: Option<RoutingMetadata>,
pub created_by: String,
pub algorithm: Program,
pub algorithm: StaticRoutingAlgorithm,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum StaticRoutingAlgorithm {
Advanced(Program),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -736,6 +807,15 @@ impl TryFrom<ast::Program<ConnectorSelection>> for Program {
}
}
impl TryFrom<ast::Program<ConnectorSelection>> for StaticRoutingAlgorithm {
type Error = error_stack::Report<errors::RoutingError>;
fn try_from(p: ast::Program<ConnectorSelection>) -> Result<Self, Self::Error> {
let internal_program: Program = p.try_into()?;
Ok(Self::Advanced(internal_program))
}
}
fn convert_rule(rule: ast::Rule<ConnectorSelection>) -> RoutingResult<Rule> {
let routing_type = match &rule.connector_selection {
ConnectorSelection::Priority(_) => RoutingType::Priority,
@ -827,6 +907,10 @@ fn convert_output(sel: ConnectorSelection) -> Output {
}
}
fn stringify_choice(c: RoutableConnectorChoice) -> String {
c.connector.to_string()
fn stringify_choice(c: RoutableConnectorChoice) -> ConnectorInfo {
ConnectorInfo::new(
c.connector.to_string(),
c.merchant_connector_id
.map(|mca_id| mca_id.get_string_repr().to_string()),
)
}

View File

@ -359,13 +359,34 @@ pub async fn create_routing_algorithm_under_profile(
}),
};
decision_engine_routing_id = create_de_euclid_routing_algo(&state, &routing_rule)
.await
.map_err(|e| {
// errors are ignored as this is just for diff checking as of now (optional flow).
logger::error!(decision_engine_error=?e,decision_engine_euclid_request=?routing_rule, "failed to create rule in decision_engine");
})
.ok();
match create_de_euclid_routing_algo(&state, &routing_rule).await {
Ok(id) => {
decision_engine_routing_id = Some(id);
}
Err(e)
if matches!(
e.current_context(),
errors::RoutingError::DecisionEngineValidationError(_)
) =>
{
if let errors::RoutingError::DecisionEngineValidationError(msg) =
e.current_context()
{
logger::error!(
decision_engine_euclid_error = ?msg,
decision_engine_euclid_request = ?routing_rule,
"failed to create rule in decision_engine with validation error"
);
}
}
Err(e) => {
logger::error!(
decision_engine_euclid_error = ?e,
decision_engine_euclid_request = ?routing_rule,
"failed to create rule in decision_engine"
);
}
}
}
Err(e) => {
// errors are ignored as this is just for diff checking as of now (optional flow).
@ -2363,7 +2384,7 @@ pub async fn migrate_rules_for_profile(
name: algorithm.name.clone(),
description: algorithm.description.clone(),
created_by: profile_id.get_string_repr().to_string(),
algorithm: internal_program,
algorithm: StaticRoutingAlgorithm::Advanced(internal_program),
metadata: None,
};