feat(routing): add profile config to switch between HS routing and DE routing result (#8350)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: prajjwalkumar17 <prajjwal.kumar@juspay.in>
Co-authored-by: Prajjwal kumar <write2prajjwal@gmail.com>
This commit is contained in:
Jagan
2025-06-20 18:19:23 +05:30
committed by GitHub
parent 5b8ba55f4e
commit a721d90c6b
7 changed files with 708 additions and 388 deletions

View File

@ -186,6 +186,19 @@ pub struct DecisionEngineConfigSetupRequest {
pub config: DecisionEngineConfigVariant,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GetDecisionEngineConfigRequest {
pub merchant_id: String,
pub config: DecisionEngineDynamicAlgorithmType,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub enum DecisionEngineDynamicAlgorithmType {
SuccessRate,
Elimination,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", content = "data")]
#[serde(rename_all = "camelCase")]

View File

@ -1409,6 +1409,7 @@ pub struct RuleMigrationResponse {
pub profile_id: common_utils::id_type::ProfileId,
pub euclid_algorithm_id: common_utils::id_type::RoutingId,
pub decision_engine_algorithm_id: String,
pub is_active_rule: bool,
}
#[derive(Debug, serde::Serialize)]
@ -1423,11 +1424,23 @@ impl RuleMigrationResponse {
profile_id: common_utils::id_type::ProfileId,
euclid_algorithm_id: common_utils::id_type::RoutingId,
decision_engine_algorithm_id: String,
is_active_rule: bool,
) -> Self {
Self {
profile_id,
euclid_algorithm_id,
decision_engine_algorithm_id,
is_active_rule,
}
}
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, strum::Display, strum::EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RoutingResultSource {
/// External Decision Engine
DecisionEngine,
/// Inbuilt Hyperswitch Routing Engine
HyperswitchRouting,
}

View File

@ -5,7 +5,7 @@ use common_utils::{
date_time,
encryption::Encryption,
errors::{CustomResult, ValidationError},
ext_traits::OptionExt,
ext_traits::{OptionExt, ValueExt},
pii, type_name,
types::keymanager,
};
@ -20,8 +20,10 @@ use diesel_models::business_profile::{
use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret};
use crate::type_encryption::{crypto_operation, AsyncLift, CryptoOperation};
use crate::{
errors::api_error_response,
type_encryption::{crypto_operation, AsyncLift, CryptoOperation},
};
#[cfg(feature = "v1")]
#[derive(Clone, Debug)]
pub struct Profile {
@ -1194,6 +1196,61 @@ impl Profile {
pub fn is_vault_sdk_enabled(&self) -> bool {
self.external_vault_connector_details.is_some()
}
#[cfg(feature = "v1")]
pub fn get_payment_routing_algorithm(
&self,
) -> CustomResult<
Option<api_models::routing::RoutingAlgorithmRef>,
api_error_response::ApiErrorResponse,
> {
self.routing_algorithm
.clone()
.map(|val| {
val.parse_value::<api_models::routing::RoutingAlgorithmRef>("RoutingAlgorithmRef")
})
.transpose()
.change_context(api_error_response::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize routing algorithm ref from merchant account")
}
#[cfg(feature = "v1")]
pub fn get_payout_routing_algorithm(
&self,
) -> CustomResult<
Option<api_models::routing::RoutingAlgorithmRef>,
api_error_response::ApiErrorResponse,
> {
self.payout_routing_algorithm
.clone()
.map(|val| {
val.parse_value::<api_models::routing::RoutingAlgorithmRef>("RoutingAlgorithmRef")
})
.transpose()
.change_context(api_error_response::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to deserialize payout routing algorithm ref from merchant account",
)
}
#[cfg(feature = "v1")]
pub fn get_frm_routing_algorithm(
&self,
) -> CustomResult<
Option<api_models::routing::RoutingAlgorithmRef>,
api_error_response::ApiErrorResponse,
> {
self.frm_routing_algorithm
.clone()
.map(|val| {
val.parse_value::<api_models::routing::RoutingAlgorithmRef>("RoutingAlgorithmRef")
})
.transpose()
.change_context(api_error_response::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to deserialize frm routing algorithm ref from merchant account",
)
}
}
#[cfg(feature = "v2")]

View File

@ -460,76 +460,72 @@ pub async fn perform_static_routing_v1(
)
.await?;
Ok(match cached_algorithm.as_ref() {
let backend_input = match transaction_data {
routing::TransactionData::Payment(payment_data) => make_dsl_input(payment_data)?,
#[cfg(feature = "payouts")]
routing::TransactionData::Payout(payout_data) => make_dsl_input_for_payouts(payout_data)?,
};
let payment_id = match transaction_data {
routing::TransactionData::Payment(payment_data) => payment_data
.payment_attempt
.payment_id
.clone()
.get_string_repr()
.to_string(),
#[cfg(feature = "payouts")]
routing::TransactionData::Payout(payout_data) => {
payout_data.payout_attempt.payout_id.clone()
}
};
let routing_events_wrapper = utils::RoutingEventsWrapper::new(
state.tenant.tenant_id.clone(),
state.request_id,
payment_id,
business_profile.get_id().to_owned(),
business_profile.merchant_id.to_owned(),
"DecisionEngine: Euclid Static Routing".to_string(),
None,
true,
false,
);
let de_euclid_connectors = perform_decision_euclid_routing(
state,
backend_input.clone(),
business_profile.get_id().get_string_repr().to_string(),
routing_events_wrapper
)
.await
.map_err(|e|
// errors are ignored as this is just for diff checking as of now (optional flow).
logger::error!(decision_engine_euclid_evaluate_error=?e, "decision_engine_euclid: error in evaluation of rule")
).unwrap_or_default();
let routable_connectors = match cached_algorithm.as_ref() {
CachedAlgorithm::Single(conn) => vec![(**conn).clone()],
CachedAlgorithm::Priority(plist) => plist.clone(),
CachedAlgorithm::VolumeSplit(splits) => perform_volume_split(splits.to_vec())
.change_context(errors::RoutingError::ConnectorSelectionFailed)?,
CachedAlgorithm::Advanced(interpreter) => {
let backend_input = match transaction_data {
routing::TransactionData::Payment(payment_data) => make_dsl_input(payment_data)?,
#[cfg(feature = "payouts")]
routing::TransactionData::Payout(payout_data) => {
make_dsl_input_for_payouts(payout_data)?
}
};
let payment_id = match transaction_data {
routing::TransactionData::Payment(payment_data) => payment_data
.payment_attempt
.payment_id
.clone()
.get_string_repr()
.to_string(),
#[cfg(feature = "payouts")]
routing::TransactionData::Payout(payout_data) => {
payout_data.payout_attempt.payout_id.clone()
}
};
let routing_events_wrapper = utils::RoutingEventsWrapper::new(
state.tenant.tenant_id.clone(),
state.request_id,
payment_id,
business_profile.get_id().to_owned(),
business_profile.merchant_id.to_owned(),
"DecisionEngine: Euclid Static Routing".to_string(),
None,
true,
false,
);
let de_euclid_connectors = perform_decision_euclid_routing(
state,
backend_input.clone(),
business_profile.get_id().get_string_repr().to_string(),
routing_events_wrapper
)
.await
.map_err(|e|
// errors are ignored as this is just for diff checking as of now (optional flow).
logger::error!(decision_engine_euclid_evaluate_error=?e, "decision_engine_euclid: error in evaluation of rule")
).unwrap_or_default();
let routable_connectors = execute_dsl_and_get_connector_v1(backend_input, interpreter)?;
let connectors = routable_connectors
.iter()
.map(|c| c.connector.to_string())
.collect::<Vec<String>>();
let de_connectors = de_euclid_connectors
.iter()
.map(|c| c.gateway_name.to_string())
.collect::<Vec<String>>();
utils::compare_and_log_result(
de_connectors,
connectors,
"evaluate_routing".to_string(),
);
routable_connectors
execute_dsl_and_get_connector_v1(backend_input, interpreter)?
}
})
};
utils::compare_and_log_result(
de_euclid_connectors.clone(),
routable_connectors.clone(),
"evaluate_routing".to_string(),
);
Ok(utils::select_routing_result(
state,
business_profile,
routable_connectors,
de_euclid_connectors,
)
.await)
}
async fn ensure_algorithm_cached_v1(

View File

@ -1,19 +1,26 @@
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use std::collections::{HashMap, HashSet};
use api_models::{
open_router as or_types, routing as api_routing,
routing::{ConnectorSelection, RoutableConnectorChoice},
open_router as or_types,
routing::{
self as api_routing, ConnectorSelection, ConnectorVolumeSplit, RoutableConnectorChoice,
},
};
use async_trait::async_trait;
use common_utils::{ext_traits::BytesExt, id_type};
use common_enums::TransactionType;
use common_utils::{
ext_traits::{BytesExt, StringExt},
id_type,
};
use diesel_models::{enums, routing_algorithm};
use error_stack::ResultExt;
use euclid::{backend::BackendInput, frontend::ast};
use euclid::{
backend::BackendInput,
frontend::ast::{self},
};
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
use external_services::grpc_client::dynamic_routing as ir_client;
use hyperswitch_domain_models::business_profile;
use hyperswitch_interfaces::events::routing_api_logs as routing_events;
use router_env::tracing_actix_web::RequestId;
use serde::{Deserialize, Serialize};
@ -40,17 +47,6 @@ pub trait DecisionEngineApiHandler {
where
Req: Serialize + Send + Sync + 'static + Clone,
Res: Serialize + serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug + Clone;
async fn send_decision_engine_request_without_response_parsing<Req>(
state: &SessionState,
http_method: services::Method,
path: &str,
request_body: Option<Req>,
timeout: Option<u64>,
events_wrapper: Option<RoutingEventsWrapper<Req>>,
) -> RoutingResult<()>
where
Req: Serialize + Send + Sync + 'static + Clone;
}
// Struct to implement the DecisionEngineApiHandler trait
@ -71,7 +67,7 @@ pub async fn build_and_send_decision_engine_http_request<Req, Res, ErrRes>(
) -> RoutingResult<RoutingEventsResponse<Res>>
where
Req: Serialize + Send + Sync + 'static + Clone,
Res: Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Clone,
Res: Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Clone + 'static,
ErrRes: serde::de::DeserializeOwned + std::fmt::Debug + Clone + DecisionEngineErrorsInterface,
{
let decision_engine_base_url = &state.conf.open_router.url;
@ -109,6 +105,15 @@ where
let resp = should_parse_response
.then(|| {
if std::any::TypeId::of::<Res>() == std::any::TypeId::of::<String>()
&& resp.response.is_empty()
{
return serde_json::from_str::<Res>("\"\"").change_context(
errors::RoutingError::OpenRouterError(
"Failed to parse empty response as String".into(),
),
);
}
let response_type: Res = resp
.response
.parse_struct(std::any::type_name::<Res>())
@ -209,35 +214,6 @@ impl DecisionEngineApiHandler for EuclidApiClient {
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(event_response)
}
async fn send_decision_engine_request_without_response_parsing<Req>(
state: &SessionState,
http_method: services::Method,
path: &str,
request_body: Option<Req>,
timeout: Option<u64>,
events_wrapper: Option<RoutingEventsWrapper<Req>>,
) -> RoutingResult<()>
where
Req: Serialize + Send + Sync + 'static + Clone,
{
let event_response =
build_and_send_decision_engine_http_request::<Req, (), DeErrorResponse>(
state,
http_method,
path,
request_body,
timeout,
"not parsing response",
events_wrapper,
)
.await?;
let response = event_response.response;
logger::debug!(euclid_response = ?response, euclid_request_path = %path, "decision_engine_routing: Received raw response from Euclid API");
Ok(())
}
}
#[async_trait]
@ -275,35 +251,6 @@ impl DecisionEngineApiHandler for ConfigApiClient {
logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::<Res>(), decision_engine_request_path = %path, "decision_engine_config: Successfully parsed response from Decision Engine config API");
Ok(events_response)
}
async fn send_decision_engine_request_without_response_parsing<Req>(
state: &SessionState,
http_method: services::Method,
path: &str,
request_body: Option<Req>,
timeout: Option<u64>,
events_wrapper: Option<RoutingEventsWrapper<Req>>,
) -> RoutingResult<()>
where
Req: Serialize + Send + Sync + 'static + Clone,
{
let event_response =
build_and_send_decision_engine_http_request::<Req, (), DeErrorResponse>(
state,
http_method,
path,
request_body,
timeout,
"not parsing response",
events_wrapper,
)
.await?;
let response = event_response.response;
logger::debug!(decision_engine_response = ?response, decision_engine_request_path = %path, "decision_engine_config: Received raw response from Decision Engine config API");
Ok(())
}
}
#[async_trait]
@ -342,35 +289,6 @@ impl DecisionEngineApiHandler for SRApiClient {
logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::<Res>(), decision_engine_request_path = %path, "decision_engine_config: Successfully parsed response from Decision Engine config API");
Ok(events_response)
}
async fn send_decision_engine_request_without_response_parsing<Req>(
state: &SessionState,
http_method: services::Method,
path: &str,
request_body: Option<Req>,
timeout: Option<u64>,
events_wrapper: Option<RoutingEventsWrapper<Req>>,
) -> RoutingResult<()>
where
Req: Serialize + Send + Sync + 'static + Clone,
{
let event_response =
build_and_send_decision_engine_http_request::<Req, (), or_types::ErrorResponse>(
state,
http_method,
path,
request_body,
timeout,
"not parsing response",
events_wrapper,
)
.await?;
let response = event_response.response;
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;
@ -380,7 +298,7 @@ pub async fn perform_decision_euclid_routing(
input: BackendInput,
created_by: String,
events_wrapper: RoutingEventsWrapper<RoutingEvaluateRequest>,
) -> RoutingResult<Vec<ConnectorInfo>> {
) -> RoutingResult<Vec<RoutableConnectorChoice>> {
logger::debug!("decision_engine_euclid: evaluate api call for euclid routing evaluation");
let mut events_wrapper = events_wrapper;
@ -413,47 +331,8 @@ pub async fn perform_decision_euclid_routing(
status_code: 500,
})?;
let connector_info = euclid_response.evaluated_output.clone();
let mut routable_connectors = Vec::new();
for conn in &connector_info {
let connector = common_enums::RoutableConnectors::from_str(conn.gateway_name.as_str())
.change_context(errors::RoutingError::GenericConversionError {
from: "String".to_string(),
to: "RoutableConnectors".to_string(),
})
.attach_printable(
"decision_engine_euclid: unable to convert String to RoutableConnectors",
)
.ok();
let mca_id = conn
.gateway_id
.as_ref()
.map(|id| {
id_type::MerchantConnectorAccountId::wrap(id.to_string())
.change_context(errors::RoutingError::GenericConversionError {
from: "String".to_string(),
to: "MerchantConnectorAccountId".to_string(),
})
.attach_printable(
"decision_engine_euclid: unable to convert MerchantConnectorAccountId from string",
)
})
.transpose()
.ok()
.flatten();
if let Some(conn) = connector {
let connector = RoutableConnectorChoice {
choice_kind: api_routing::RoutableChoiceKind::FullStruct,
connector: conn,
merchant_connector_id: mca_id,
};
routable_connectors.push(connector);
}
}
routing_event.set_routing_approach(RoutingApproach::StaticRouting.to_string());
routing_event.set_routable_connectors(routable_connectors);
routing_event.set_routable_connectors(euclid_response.evaluated_output.clone());
state.event_handler.log_event(&routing_event);
// Need to log euclid response event here
@ -498,7 +377,7 @@ pub async fn link_de_euclid_routing_algorithm(
) -> RoutingResult<()> {
logger::debug!("decision_engine_euclid: link api call for euclid routing algorithm");
EuclidApiClient::send_decision_engine_request_without_response_parsing(
EuclidApiClient::send_decision_engine_request::<_, String>(
state,
services::Method::Post,
"routing/activate",
@ -542,6 +421,31 @@ pub async fn list_de_euclid_routing_algorithms(
.collect::<Vec<_>>())
}
pub async fn list_de_euclid_active_routing_algorithm(
state: &SessionState,
created_by: String,
) -> RoutingResult<Vec<api_routing::RoutingDictionaryRecord>> {
logger::debug!("decision_engine_euclid: list api call for euclid active routing algorithm");
let response: Vec<RoutingAlgorithmRecord> = EuclidApiClient::send_decision_engine_request(
state,
services::Method::Post,
format!("routing/list/active/{created_by}").as_str(),
None::<()>,
Some(EUCLID_API_TIMEOUT),
None,
)
.await?
.response
.ok_or(errors::RoutingError::OpenRouterError(
"Response from decision engine API is empty".to_string(),
))?;
Ok(response
.into_iter()
.map(|record| routing_algorithm::RoutingProfileMetadata::from(record).foreign_into())
.collect())
}
pub fn compare_and_log_result<T: RoutingEq<T> + Serialize>(
de_result: Vec<T>,
result: Vec<T>,
@ -566,7 +470,8 @@ pub trait RoutingEq<T> {
impl RoutingEq<Self> for api_routing::RoutingDictionaryRecord {
fn is_equal(a: &Self, b: &Self) -> bool {
a.name == b.name
a.id == b.id
&& a.name == b.name
&& a.profile_id == b.profile_id
&& a.description == b.description
&& a.kind == b.kind
@ -580,6 +485,14 @@ impl RoutingEq<Self> for String {
}
}
impl RoutingEq<Self> for RoutableConnectorChoice {
fn is_equal(a: &Self, b: &Self) -> bool {
a.connector.eq(&b.connector)
&& a.choice_kind.eq(&b.choice_kind)
&& a.merchant_connector_id.eq(&b.merchant_connector_id)
}
}
pub fn to_json_string<T: Serialize>(value: &T) -> String {
serde_json::to_string(value)
.map_err(|_| errors::RoutingError::GenericConversionError {
@ -772,8 +685,30 @@ pub struct RoutingEvaluateRequest {
pub struct RoutingEvaluateResponse {
pub status: String,
pub output: serde_json::Value,
pub evaluated_output: Vec<ConnectorInfo>,
pub eligible_connectors: Vec<ConnectorInfo>,
#[serde(deserialize_with = "deserialize_connector_choices")]
pub evaluated_output: Vec<RoutableConnectorChoice>,
#[serde(deserialize_with = "deserialize_connector_choices")]
pub eligible_connectors: Vec<RoutableConnectorChoice>,
}
/// Routable Connector chosen for a payment
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DeRoutableConnectorChoice {
pub gateway_name: common_enums::RoutableConnectors,
pub gateway_id: Option<id_type::MerchantConnectorAccountId>,
}
fn deserialize_connector_choices<'de, D>(
deserializer: D,
) -> Result<Vec<RoutableConnectorChoice>, D::Error>
where
D: serde::Deserializer<'de>,
{
let infos = Vec::<DeRoutableConnectorChoice>::deserialize(deserializer)?;
Ok(infos
.into_iter()
.map(RoutableConnectorChoice::from)
.collect())
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -794,12 +729,24 @@ pub enum ValueType {
MetadataVariant(MetadataValue),
/// Represents a arbitrary String value
StrValue(String),
/// Represents a global reference, which is a reference to a global variable
GlobalRef(String),
/// Represents an array of numbers. This is basically used for
/// "one of the given numbers" operations
/// eg: payment.method.amount = (1, 2, 3)
NumberArray(Vec<u64>),
/// Similar to NumberArray but for enum variants
/// eg: payment.method.cardtype = (debit, credit)
EnumVariantArray(Vec<String>),
/// Like a number array but can include comparisons. Useful for
/// conditions like "500 < amount < 1000"
/// eg: payment.amount = (> 500, < 1000)
NumberComparisonArray(Vec<NumberComparison>),
}
pub type Metadata = HashMap<String, serde_json::Value>;
/// Represents a number comparison for "NumberComparisonArrayValue"
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct NumberComparison {
pub comparison_type: ComparisonType,
@ -917,9 +864,20 @@ impl ConnectorInfo {
}
}
impl From<DeRoutableConnectorChoice> for RoutableConnectorChoice {
fn from(choice: DeRoutableConnectorChoice) -> Self {
Self {
choice_kind: api_routing::RoutableChoiceKind::FullStruct,
connector: choice.gateway_name,
merchant_connector_id: choice.gateway_id,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Output {
Single(ConnectorInfo),
Priority(Vec<ConnectorInfo>),
VolumeSplit(Vec<VolumeSplit<ConnectorInfo>>),
VolumeSplitPriority(Vec<VolumeSplit<Vec<ConnectorInfo>>>),
@ -949,12 +907,73 @@ pub struct RoutingRule {
pub description: Option<String>,
pub metadata: Option<RoutingMetadata>,
pub created_by: String,
#[serde(default)]
pub algorithm_for: AlgorithmType,
pub algorithm: StaticRoutingAlgorithm,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, strum::Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum AlgorithmType {
#[default]
Payment,
Payout,
ThreeDsAuthentication,
}
impl From<TransactionType> for AlgorithmType {
fn from(transaction_type: TransactionType) -> Self {
match transaction_type {
TransactionType::Payment => Self::Payment,
TransactionType::Payout => Self::Payout,
TransactionType::ThreeDsAuthentication => Self::ThreeDsAuthentication,
}
}
}
impl From<RoutableConnectorChoice> for ConnectorInfo {
fn from(c: RoutableConnectorChoice) -> Self {
Self {
gateway_name: c.connector.to_string(),
gateway_id: c
.merchant_connector_id
.map(|mca_id| mca_id.get_string_repr().to_string()),
}
}
}
impl From<Box<RoutableConnectorChoice>> for ConnectorInfo {
fn from(c: Box<RoutableConnectorChoice>) -> Self {
Self {
gateway_name: c.connector.to_string(),
gateway_id: c
.merchant_connector_id
.map(|mca_id| mca_id.get_string_repr().to_string()),
}
}
}
impl From<ConnectorVolumeSplit> for VolumeSplit<ConnectorInfo> {
fn from(v: ConnectorVolumeSplit) -> Self {
Self {
split: v.split,
output: ConnectorInfo {
gateway_name: v.connector.connector.to_string(),
gateway_id: v
.connector
.merchant_connector_id
.map(|mca_id| mca_id.get_string_repr().to_string()),
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum StaticRoutingAlgorithm {
Single(Box<ConnectorInfo>),
Priority(Vec<ConnectorInfo>),
VolumeSplit(Vec<VolumeSplit<ConnectorInfo>>),
Advanced(Program),
}
@ -962,7 +981,6 @@ pub enum StaticRoutingAlgorithm {
#[serde(rename_all = "snake_case")]
pub struct RoutingMetadata {
pub kind: enums::RoutingAlgorithmKind,
pub algorithm_for: enums::TransactionType,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@ -981,7 +999,8 @@ pub struct RoutingAlgorithmRecord {
pub name: String,
pub description: Option<String>,
pub created_by: id_type::ProfileId,
pub algorithm_data: Program,
pub algorithm_data: StaticRoutingAlgorithm,
pub algorithm_for: TransactionType,
pub metadata: Option<RoutingMetadata>,
pub created_at: time::PrimitiveDateTime,
pub modified_at: time::PrimitiveDateTime,
@ -989,12 +1008,11 @@ pub struct RoutingAlgorithmRecord {
impl From<RoutingAlgorithmRecord> for routing_algorithm::RoutingProfileMetadata {
fn from(record: RoutingAlgorithmRecord) -> Self {
let (kind, algorithm_for) = match record.metadata {
Some(metadata) => (metadata.kind, metadata.algorithm_for),
None => (
enums::RoutingAlgorithmKind::Advanced,
enums::TransactionType::default(),
),
let kind = match record.algorithm_data {
StaticRoutingAlgorithm::Single(_) => enums::RoutingAlgorithmKind::Single,
StaticRoutingAlgorithm::Priority(_) => enums::RoutingAlgorithmKind::Priority,
StaticRoutingAlgorithm::VolumeSplit(_) => enums::RoutingAlgorithmKind::VolumeSplit,
StaticRoutingAlgorithm::Advanced(_) => enums::RoutingAlgorithmKind::Advanced,
};
Self {
profile_id: record.created_by,
@ -1004,7 +1022,7 @@ impl From<RoutingAlgorithmRecord> for routing_algorithm::RoutingProfileMetadata
kind,
created_at: record.created_at,
modified_at: record.modified_at,
algorithm_for,
algorithm_for: record.algorithm_for,
}
}
}
@ -1106,8 +1124,20 @@ fn convert_value(v: ast::ValueType) -> RoutingResult<ValueType> {
value: m.value,
})),
StrValue(s) => Ok(ValueType::StrValue(s)),
_ => Err(error_stack::Report::new(
errors::RoutingError::InvalidRoutingAlgorithmStructure,
NumberArray(arr) => Ok(ValueType::NumberArray(
arr.into_iter()
.map(|n| n.get_amount_as_i64().try_into().unwrap_or_default())
.collect(),
)),
EnumVariantArray(arr) => Ok(ValueType::EnumVariantArray(arr)),
NumberComparisonArray(arr) => Ok(ValueType::NumberComparisonArray(
arr.into_iter()
.map(|nc| NumberComparison {
comparison_type: convert_comparison_type(nc.comparison_type),
number: nc.number.get_amount_as_i64().try_into().unwrap_or_default(),
})
.collect(),
)),
}
}
@ -1136,6 +1166,29 @@ fn stringify_choice(c: RoutableConnectorChoice) -> ConnectorInfo {
)
}
pub async fn select_routing_result<T>(
state: &SessionState,
business_profile: &business_profile::Profile,
hyperswitch_result: T,
de_result: T,
) -> T {
let routing_result_source: Option<api_routing::RoutingResultSource> = state
.store
.find_config_by_key(&format!(
"routing_result_source_{0}",
business_profile.get_id().get_string_repr()
))
.await
.map(|c| c.config.parse_enum("RoutingResultSource").ok())
.unwrap_or(None); //Ignore errors so that we can use the hyperswitch result as a fallback
if let Some(api_routing::RoutingResultSource::DecisionEngine) = routing_result_source {
logger::debug!(business_profile_id=?business_profile.get_id(), "Using Decision Engine routing result");
de_result
} else {
logger::debug!(business_profile_id=?business_profile.get_id(), "Using Hyperswitch routing result");
hyperswitch_result
}
}
pub trait DecisionEngineErrorsInterface {
fn get_error_message(&self) -> String;
fn get_error_code(&self) -> String;

View File

@ -25,8 +25,6 @@ use external_services::grpc_client::dynamic_routing::{
use helpers::update_decision_engine_dynamic_routing_setup;
use hyperswitch_domain_models::{mandates, payment_address};
use payment_methods::helpers::StorageErrorExt;
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
use router_env::logger;
use rustc_hash::FxHashSet;
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
use storage_impl::redis::cache;
@ -167,7 +165,7 @@ pub async fn retrieve_merchant_routing_dictionary(
routing_metadata,
);
let result = routing_metadata
let mut result = routing_metadata
.into_iter()
.map(ForeignInto::foreign_into)
.collect::<Vec<_>>();
@ -175,7 +173,7 @@ pub async fn retrieve_merchant_routing_dictionary(
if let Some(profile_ids) = profile_id_list {
let mut de_result: Vec<routing_types::RoutingDictionaryRecord> = vec![];
// DE_TODO: need to replace this with batch API call to reduce the number of network calls
for profile_id in profile_ids {
for profile_id in &profile_ids {
let list_request = ListRountingAlgorithmsRequest {
created_by: profile_id.get_string_repr().to_string(),
};
@ -186,8 +184,32 @@ pub async fn retrieve_merchant_routing_dictionary(
})
.ok() // Avoid throwing error if Decision Engine is not available or other errors
.map(|mut de_routing| de_result.append(&mut de_routing));
// filter de_result based on transaction type
de_result.retain(|record| record.algorithm_for == Some(transaction_type));
// append dynamic routing algorithms to de_result
de_result.append(
&mut result
.clone()
.into_iter()
.filter(|record: &routing_types::RoutingDictionaryRecord| {
record.kind == routing_types::RoutingAlgorithmKind::Dynamic
})
.collect::<Vec<_>>(),
);
}
compare_and_log_result(de_result, result.clone(), "list_routing".to_string());
compare_and_log_result(
de_result.clone(),
result.clone(),
"list_routing".to_string(),
);
result = build_list_routing_result(
&state,
merchant_context,
&result,
&de_result,
profile_ids.clone(),
)
.await?;
}
metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add(1, &[]);
@ -196,6 +218,47 @@ pub async fn retrieve_merchant_routing_dictionary(
))
}
async fn build_list_routing_result(
state: &SessionState,
merchant_context: domain::MerchantContext,
hs_results: &[routing_types::RoutingDictionaryRecord],
de_results: &[routing_types::RoutingDictionaryRecord],
profile_ids: Vec<common_utils::id_type::ProfileId>,
) -> RouterResult<Vec<routing_types::RoutingDictionaryRecord>> {
let db = state.store.as_ref();
let key_manager_state = &state.into();
let mut list_result: Vec<routing_types::RoutingDictionaryRecord> = vec![];
for profile_id in profile_ids.iter() {
let by_profile =
|rec: &&routing_types::RoutingDictionaryRecord| &rec.profile_id == profile_id;
let de_result_for_profile = de_results.iter().filter(by_profile).cloned().collect();
let hs_result_for_profile = hs_results.iter().filter(by_profile).cloned().collect();
let business_profile = core_utils::validate_and_get_business_profile(
db,
key_manager_state,
merchant_context.get_merchant_key_store(),
Some(profile_id),
merchant_context.get_merchant_account().get_id(),
)
.await?
.get_required_value("Profile")
.change_context(errors::ApiErrorResponse::ProfileNotFound {
id: profile_id.get_string_repr().to_owned(),
})?;
list_result.append(
&mut select_routing_result(
state,
&business_profile,
hs_result_for_profile,
de_result_for_profile,
)
.await,
);
}
Ok(list_result)
}
#[cfg(feature = "v2")]
pub async fn create_routing_algorithm_under_profile(
state: SessionState,
@ -280,8 +343,6 @@ pub async fn create_routing_algorithm_under_profile(
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
use api_models::routing::StaticRoutingAlgorithm as EuclidAlgorithm;
use crate::services::logger;
metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(1, &[]);
let db = state.store.as_ref();
let key_manager_state = &(&state).into();
@ -344,61 +405,84 @@ pub async fn create_routing_algorithm_under_profile(
let mut decision_engine_routing_id: Option<String> = None;
if let Some(EuclidAlgorithm::Advanced(program)) = request.algorithm.clone() {
match program.try_into() {
Ok(internal_program) => {
let routing_rule = RoutingRule {
rule_id: None,
name: name.clone(),
description: Some(description.clone()),
created_by: profile_id.get_string_repr().to_string(),
algorithm: internal_program,
metadata: Some(RoutingMetadata {
kind: algorithm.get_kind().foreign_into(),
algorithm_for: transaction_type.to_owned(),
}),
};
if let Some(euclid_algorithm) = request.algorithm.clone() {
let maybe_static_algorithm: Option<StaticRoutingAlgorithm> = match euclid_algorithm {
EuclidAlgorithm::Advanced(program) => match program.try_into() {
Ok(internal_program) => Some(StaticRoutingAlgorithm::Advanced(internal_program)),
Err(e) => {
router_env::logger::error!(decision_engine_error = ?e, "decision_engine_euclid");
None
}
},
EuclidAlgorithm::Single(conn) => {
Some(StaticRoutingAlgorithm::Single(Box::new(conn.into())))
}
EuclidAlgorithm::Priority(connectors) => {
let converted: Vec<ConnectorInfo> =
connectors.into_iter().map(Into::into).collect();
Some(StaticRoutingAlgorithm::Priority(converted))
}
EuclidAlgorithm::VolumeSplit(splits) => {
let converted: Vec<VolumeSplit<ConnectorInfo>> =
splits.into_iter().map(Into::into).collect();
Some(StaticRoutingAlgorithm::VolumeSplit(converted))
}
EuclidAlgorithm::ThreeDsDecisionRule(_) => {
router_env::logger::error!(
"decision_engine_euclid: ThreeDsDecisionRules are not yet implemented"
);
None
}
};
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 Some(static_algorithm) = maybe_static_algorithm {
let routing_rule = RoutingRule {
rule_id: Some(algorithm_id.clone().get_string_repr().to_owned()),
name: name.clone(),
description: Some(description.clone()),
created_by: profile_id.get_string_repr().to_string(),
algorithm: static_algorithm,
algorithm_for: transaction_type.into(),
metadata: Some(RoutingMetadata {
kind: algorithm.get_kind().foreign_into(),
}),
};
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()
{
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,
router_env::logger::error!(
decision_engine_euclid_error = ?msg,
decision_engine_euclid_request = ?routing_rule,
"failed to create rule in decision_engine"
"failed to create rule in decision_engine with validation error"
);
}
}
Err(e) => {
router_env::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).
logger::error!(decision_engine_error=?e, "decision_engine_euclid");
}
};
}
}
if decision_engine_routing_id.is_some() {
logger::info!(routing_flow=?"create_euclid_routing_algorithm", is_equal=?"true", "decision_engine_euclid");
router_env::logger::info!(routing_flow=?"create_euclid_routing_algorithm", is_equal=?"true", "decision_engine_euclid");
} else {
logger::info!(routing_flow=?"create_euclid_routing_algorithm", is_equal=?"false", "decision_engine_euclid");
router_env::logger::info!(routing_flow=?"create_euclid_routing_algorithm", is_equal=?"false", "decision_engine_euclid");
}
let timestamp = common_utils::date_time::now();
@ -1276,7 +1360,23 @@ pub async fn retrieve_linked_routing_config(
)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
active_algorithms.push(record.foreign_into());
let hs_records: Vec<routing_types::RoutingDictionaryRecord> =
vec![record.foreign_into()];
let de_records = retrieve_decision_engine_active_rules(
&state,
&transaction_type,
profile_id.clone(),
hs_records.clone(),
)
.await;
compare_and_log_result(
de_records.clone(),
hs_records.clone(),
"list_active_routing".to_string(),
);
active_algorithms.append(
&mut select_routing_result(&state, &business_profile, hs_records, de_records).await,
);
}
// Handle dynamic routing algorithms
@ -1319,7 +1419,9 @@ pub async fn retrieve_linked_routing_config(
)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
active_algorithms.push(record.foreign_into());
if record.algorithm_for == transaction_type {
active_algorithms.push(record.foreign_into());
}
}
}
@ -1328,6 +1430,32 @@ pub async fn retrieve_linked_routing_config(
routing_types::LinkedRoutingConfigRetrieveResponse::ProfileBased(active_algorithms),
))
}
pub async fn retrieve_decision_engine_active_rules(
state: &SessionState,
transaction_type: &enums::TransactionType,
profile_id: common_utils::id_type::ProfileId,
hs_records: Vec<routing_types::RoutingDictionaryRecord>,
) -> Vec<routing_types::RoutingDictionaryRecord> {
let mut de_records =
list_de_euclid_active_routing_algorithm(state, profile_id.get_string_repr().to_owned())
.await
.map_err(|e| {
router_env::logger::error!(?e, "Failed to list DE Euclid active routing algorithm");
})
.ok() // Avoid throwing error if Decision Engine is not available or other errors thrown
.unwrap_or_default();
// Use Hs records to list the dynamic algorithms as DE is not supporting dynamic algorithms in HS standard
let mut dynamic_algos = hs_records
.into_iter()
.filter(|record| record.kind == routing_types::RoutingAlgorithmKind::Dynamic)
.collect::<Vec<_>>();
de_records.append(&mut dynamic_algos);
de_records
.into_iter()
.filter(|r| r.algorithm_for == Some(*transaction_type))
.collect::<Vec<_>>()
}
// List all the default fallback algorithms under all the profile under a merchant
pub async fn retrieve_default_routing_config_for_profiles(
state: SessionState,
@ -1707,7 +1835,7 @@ pub async fn success_based_routing_update_configs(
cache_entries_to_redact,
)
.await
.map_err(|e| logger::error!("unable to publish into the redact channel for evicting the success based routing config cache {e:?}"));
.map_err(|e| router_env::logger::error!("unable to publish into the redact channel for evicting the success based routing config cache {e:?}"));
let new_record = record.foreign_into();
@ -1811,7 +1939,7 @@ pub async fn elimination_routing_update_configs(
cache_entries_to_redact,
)
.await
.map_err(|e| logger::error!("unable to publish into the redact channel for evicting the elimination routing config cache {e:?}")).ok();
.map_err(|e| router_env::logger::error!("unable to publish into the redact channel for evicting the elimination routing config cache {e:?}")).ok();
let new_record = record.foreign_into();
@ -2148,7 +2276,7 @@ pub async fn contract_based_routing_update_configs(
cache_entries_to_redact,
)
.await
.map_err(|e| logger::error!("unable to publish into the redact channel for evicting the contract based routing config cache {e:?}"));
.map_err(|e| router_env::logger::error!("unable to publish into the redact channel for evicting the contract based routing config cache {e:?}"));
let new_record = record.foreign_into();
@ -2324,15 +2452,13 @@ pub async fn migrate_rules_for_profile(
) -> RouterResult<routing_types::RuleMigrationResult> {
use api_models::routing::StaticRoutingAlgorithm as EuclidAlgorithm;
use crate::services::logger;
let profile_id = query_params.profile_id.clone();
let db = state.store.as_ref();
let key_manager_state = &(&state).into();
let merchant_key_store = merchant_context.get_merchant_key_store();
let merchant_id = merchant_context.get_merchant_account().get_id();
core_utils::validate_and_get_business_profile(
let business_profile = core_utils::validate_and_get_business_profile(
db,
key_manager_state,
merchant_key_store,
@ -2345,7 +2471,29 @@ pub async fn migrate_rules_for_profile(
id: profile_id.get_string_repr().to_owned(),
})?;
let routing_metadatas: Vec<diesel_models::routing_algorithm::RoutingProfileMetadata> = state
#[cfg(feature = "v1")]
let active_payment_routing_ids: Vec<Option<common_utils::id_type::RoutingId>> = vec![
business_profile
.get_payment_routing_algorithm()
.attach_printable("Failed to get payment routing algorithm")?
.unwrap_or_default()
.algorithm_id,
business_profile
.get_payout_routing_algorithm()
.attach_printable("Failed to get payout routing algorithm")?
.unwrap_or_default()
.algorithm_id,
business_profile
.get_frm_routing_algorithm()
.attach_printable("Failed to get frm routing algorithm")?
.unwrap_or_default()
.algorithm_id,
];
#[cfg(feature = "v2")]
let active_payment_routing_ids = [business_profile.routing_algorithm_id.clone()];
let routing_metadatas = state
.store
.list_routing_algorithm_metadata_by_profile_id(
&profile_id,
@ -2358,111 +2506,119 @@ pub async fn migrate_rules_for_profile(
let mut response_list = Vec::new();
let mut error_list = Vec::new();
for routing_metadata in routing_metadatas
.into_iter()
.filter(|algo| algo.metadata_is_advanced_rule_for_payments())
{
match db
.find_routing_algorithm_by_profile_id_algorithm_id(
&profile_id,
&routing_metadata.algorithm_id,
)
let mut push_error = |algorithm_id, msg: String| {
error_list.push(RuleMigrationError {
profile_id: profile_id.clone(),
algorithm_id,
error: msg,
});
};
for routing_metadata in routing_metadatas {
let algorithm_id = routing_metadata.algorithm_id.clone();
let algorithm = match db
.find_routing_algorithm_by_profile_id_algorithm_id(&profile_id, &algorithm_id)
.await
{
Ok(algorithm) => {
let parsed_result = algorithm
.algorithm_data
.parse_value::<EuclidAlgorithm>("EuclidAlgorithm");
Ok(algo) => algo,
Err(e) => {
router_env::logger::error!(?e, ?algorithm_id, "Failed to fetch routing algorithm");
push_error(algorithm_id, format!("Fetch error: {:?}", e));
continue;
}
};
match parsed_result {
Ok(EuclidAlgorithm::Advanced(program)) => match program.try_into() {
Ok(internal_program) => {
let routing_rule = RoutingRule {
rule_id: Some(
algorithm.algorithm_id.clone().get_string_repr().to_string(),
),
name: algorithm.name.clone(),
description: algorithm.description.clone(),
created_by: profile_id.get_string_repr().to_string(),
algorithm: StaticRoutingAlgorithm::Advanced(internal_program),
metadata: None,
};
let parsed_result = algorithm
.algorithm_data
.parse_value::<EuclidAlgorithm>("EuclidAlgorithm");
let result = create_de_euclid_routing_algo(&state, &routing_rule).await;
match result {
Ok(decision_engine_routing_id) => {
let response = RuleMigrationResponse {
profile_id: profile_id.clone(),
euclid_algorithm_id: algorithm.algorithm_id.clone(),
decision_engine_algorithm_id: decision_engine_routing_id,
};
response_list.push(response);
}
Err(err) => {
logger::error!(
decision_engine_rule_migration_error = ?err,
algorithm_id = ?algorithm.algorithm_id,
"Failed to insert into decision engine"
);
error_list.push(RuleMigrationError {
profile_id: profile_id.clone(),
algorithm_id: algorithm.algorithm_id.clone(),
error: format!("Insertion error: {:?}", err),
});
}
}
}
Err(e) => {
logger::error!(
decision_engine_rule_migration_error = ?e,
algorithm_id = ?algorithm.algorithm_id,
"Failed to convert program"
);
error_list.push(RuleMigrationError {
profile_id: profile_id.clone(),
algorithm_id: algorithm.algorithm_id.clone(),
error: format!("Program conversion error: {:?}", e),
});
}
},
Err(e) => {
logger::error!(
decision_engine_rule_migration_error = ?e,
algorithm_id = ?algorithm.algorithm_id,
"Failed to parse EuclidAlgorithm"
);
error_list.push(RuleMigrationError {
profile_id: profile_id.clone(),
algorithm_id: algorithm.algorithm_id.clone(),
error: format!("JSON parse error: {:?}", e),
});
}
_ => {
logger::info!(
"decision_engine_rule_migration_error: Skipping non-advanced algorithm {:?}",
algorithm.algorithm_id
);
error_list.push(RuleMigrationError {
profile_id: profile_id.clone(),
algorithm_id: algorithm.algorithm_id.clone(),
error: "Not an advanced algorithm".to_string(),
});
}
let maybe_static_algorithm: Option<StaticRoutingAlgorithm> = match parsed_result {
Ok(EuclidAlgorithm::Advanced(program)) => match program.try_into() {
Ok(ip) => Some(StaticRoutingAlgorithm::Advanced(ip)),
Err(e) => {
router_env::logger::error!(
?e,
?algorithm_id,
"Failed to convert advanced program"
);
push_error(algorithm_id.clone(), format!("Conversion error: {:?}", e));
None
}
},
Ok(EuclidAlgorithm::Single(conn)) => {
Some(StaticRoutingAlgorithm::Single(Box::new(conn.into())))
}
Ok(EuclidAlgorithm::Priority(connectors)) => Some(StaticRoutingAlgorithm::Priority(
connectors.into_iter().map(Into::into).collect(),
)),
Ok(EuclidAlgorithm::VolumeSplit(splits)) => Some(StaticRoutingAlgorithm::VolumeSplit(
splits.into_iter().map(Into::into).collect(),
)),
Ok(EuclidAlgorithm::ThreeDsDecisionRule(_)) => {
router_env::logger::info!(
?algorithm_id,
"Skipping 3DS rule migration (not supported yet)"
);
push_error(algorithm_id.clone(), "3DS migration not implemented".into());
None
}
Err(e) => {
logger::error!(
decision_engine_rule_migration_error = ?e,
algorithm_id = ?routing_metadata.algorithm_id,
"Failed to fetch routing algorithm"
);
error_list.push(RuleMigrationError {
router_env::logger::error!(?e, ?algorithm_id, "Failed to parse algorithm");
push_error(algorithm_id.clone(), format!("Parse error: {:?}", e));
None
}
};
let Some(static_algorithm) = maybe_static_algorithm else {
continue;
};
let routing_rule = RoutingRule {
rule_id: Some(algorithm.algorithm_id.clone().get_string_repr().to_string()),
name: algorithm.name.clone(),
description: algorithm.description.clone(),
created_by: profile_id.get_string_repr().to_string(),
algorithm: static_algorithm,
algorithm_for: algorithm.algorithm_for.into(),
metadata: Some(RoutingMetadata {
kind: algorithm.kind,
}),
};
match create_de_euclid_routing_algo(&state, &routing_rule).await {
Ok(decision_engine_routing_id) => {
let mut is_active_rule = false;
if active_payment_routing_ids.contains(&Some(algorithm.algorithm_id.clone())) {
link_de_euclid_routing_algorithm(
&state,
ActivateRoutingConfigRequest {
created_by: profile_id.get_string_repr().to_string(),
routing_algorithm_id: decision_engine_routing_id.clone(),
},
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to link active routing algorithm")?;
is_active_rule = true;
}
response_list.push(RuleMigrationResponse {
profile_id: profile_id.clone(),
algorithm_id: routing_metadata.algorithm_id.clone(),
error: format!("Fetch error: {:?}", e),
euclid_algorithm_id: algorithm.algorithm_id.clone(),
decision_engine_algorithm_id: decision_engine_routing_id,
is_active_rule,
});
}
Err(err) => {
router_env::logger::error!(
decision_engine_rule_migration_error = ?err,
algorithm_id = ?algorithm.algorithm_id,
"Failed to insert into decision engine"
);
push_error(
algorithm.algorithm_id.clone(),
format!("Insertion error: {:?}", err),
);
}
}
}

View File

@ -72,6 +72,7 @@ pub const CONTRACT_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
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_GET_ENDPOINT: &str = "rule/get";
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";
@ -2433,6 +2434,37 @@ pub async fn update_decision_engine_dynamic_routing_setup(
Ok(())
}
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
pub async fn get_decision_engine_active_dynamic_routing_algorithm(
state: &SessionState,
profile_id: &id_type::ProfileId,
dynamic_routing_type: open_router::DecisionEngineDynamicAlgorithmType,
) -> RouterResult<Option<open_router::DecisionEngineConfigSetupRequest>> {
logger::debug!(
"decision_engine_euclid: GET api call for decision active {:?} routing algorithm",
dynamic_routing_type
);
let request = open_router::GetDecisionEngineConfigRequest {
merchant_id: profile_id.get_string_repr().to_owned(),
config: dynamic_routing_type,
};
let response: Option<open_router::DecisionEngineConfigSetupRequest> =
routing_utils::ConfigApiClient::send_decision_engine_request(
state,
services::Method::Post,
DECISION_ENGINE_RULE_GET_ENDPOINT,
Some(request),
None,
None,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get active dynamic algorithm from decision engine")?
.response;
Ok(response)
}
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
#[instrument(skip_all)]
pub async fn disable_decision_engine_dynamic_routing_setup(
@ -2545,11 +2577,11 @@ pub async fn delete_decision_engine_merchant(
DECISION_ENGINE_MERCHANT_BASE_ENDPOINT,
profile_id.get_string_repr()
);
routing_utils::ConfigApiClient::send_decision_engine_request_without_response_parsing::<()>(
routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>(
state,
services::Method::Delete,
&path,
None,
None::<id_type::ProfileId>,
None,
None,
)