diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 16ba5a41a5..3037c1d4e6 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -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, } diff --git a/crates/router/src/core/payments/routing/utils.rs b/crates/router/src/core/payments/routing/utils.rs index d34e538747..0478091936 100644 --- a/crates/router/src/core/payments/routing/utils.rs +++ b/crates/router/src/core/payments/routing/utils.rs @@ -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::() - .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::(&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::(&response_bytes) + .map_err(|_| errors::RoutingError::GenericConversionError { from: "ApiResponse".to_string(), to: std::any::type_name::().to_string(), }) @@ -127,7 +165,14 @@ impl DecisionEngineApiHandler for EuclidApiClient { path ) })?; - logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::(), 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::(), + 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, +} + //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; /// 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; /// } /// ``` #[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] pub struct IfStatement { // #[schema(value_type=Vec)] 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)] 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 { 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, +} + +impl ConnectorInfo { + pub fn new(connector: String, mca_id: Option) -> Self { + Self { connector, mca_id } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum Output { - Priority(Vec), - VolumeSplit(Vec>), - VolumeSplitPriority(Vec>>), + Priority(Vec), + VolumeSplit(Vec>), + VolumeSplitPriority(Vec>>), } pub type Globals = HashMap>; @@ -644,7 +709,7 @@ pub type Globals = HashMap>; /// 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)] pub struct Program { pub globals: Globals, @@ -663,7 +728,13 @@ pub struct RoutingRule { pub description: Option, pub metadata: Option, 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> for Program { } } +impl TryFrom> for StaticRoutingAlgorithm { + type Error = error_stack::Report; + + fn try_from(p: ast::Program) -> Result { + let internal_program: Program = p.try_into()?; + Ok(Self::Advanced(internal_program)) + } +} + fn convert_rule(rule: ast::Rule) -> RoutingResult { 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()), + ) } diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 975af0da4f..1ab333ef13 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -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, };