mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
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:
@ -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,
|
||||
}
|
||||
|
||||
@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user