refactor(dynamic_routing): add logic for creating merchant account in decision engine (#8191)

Co-authored-by: Shankar Singh C <shankar.singh@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Chethan Rao
2025-06-04 13:42:24 +05:30
committed by GitHub
parent ec908d38ac
commit 835a425ded
4 changed files with 118 additions and 32 deletions

View File

@ -608,6 +608,8 @@ pub struct DynamicRoutingAlgorithmRef {
pub dynamic_routing_volume_split: Option<u8>, pub dynamic_routing_volume_split: Option<u8>,
pub elimination_routing_algorithm: Option<EliminationRoutingAlgorithm>, pub elimination_routing_algorithm: Option<EliminationRoutingAlgorithm>,
pub contract_based_routing: Option<ContractRoutingAlgorithm>, pub contract_based_routing: Option<ContractRoutingAlgorithm>,
#[serde(default)]
pub is_merchant_created_in_decision_engine: bool,
} }
pub trait DynamicRoutingAlgoAccessor { pub trait DynamicRoutingAlgoAccessor {
@ -717,6 +719,10 @@ impl DynamicRoutingAlgorithmRef {
self.dynamic_routing_volume_split = volume self.dynamic_routing_volume_split = volume
} }
pub fn update_merchant_creation_status_in_decision_engine(&mut self, is_created: bool) {
self.is_merchant_created_in_decision_engine = is_created;
}
pub fn is_success_rate_routing_enabled(&self) -> bool { pub fn is_success_rate_routing_enabled(&self) -> bool {
self.success_based_algorithm self.success_based_algorithm
.as_ref() .as_ref()

View File

@ -264,26 +264,6 @@ pub async fn create_merchant_account(
.await .await
.to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantAccount)?; .to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantAccount)?;
// Call to DE here
// Check if creation should be based on default profile
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
{
if state.conf.open_router.enabled {
merchant_account
.default_profile
.as_ref()
.async_map(|profile_id| {
routing::helpers::create_decision_engine_merchant(&state, profile_id)
})
.await
.transpose()
.map_err(|err| {
crate::logger::error!("Failed to create merchant in Decision Engine {err:?}");
})
.ok();
}
}
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(domain::Context( let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(domain::Context(
merchant_account.clone(), merchant_account.clone(),
key_store.clone(), key_store.clone(),
@ -3904,6 +3884,22 @@ impl ProfileCreateBridge for api::ProfileCreate {
.map(CardTestingGuardConfig::foreign_from) .map(CardTestingGuardConfig::foreign_from)
.or(Some(CardTestingGuardConfig::default())); .or(Some(CardTestingGuardConfig::default()));
let mut dynamic_routing_algorithm_ref =
routing_types::DynamicRoutingAlgorithmRef::default();
if self.is_debit_routing_enabled == Some(true) {
routing::helpers::create_merchant_in_decision_engine_if_not_exists(
state,
&profile_id,
&mut dynamic_routing_algorithm_ref,
)
.await;
}
let dynamic_routing_algorithm = serde_json::to_value(dynamic_routing_algorithm_ref)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error serializing dynamic_routing_algorithm_ref to JSON Value")?;
Ok(domain::Profile::from(domain::ProfileSetter { Ok(domain::Profile::from(domain::ProfileSetter {
profile_id, profile_id,
merchant_id: merchant_context.get_merchant_account().get_id().clone(), merchant_id: merchant_context.get_merchant_account().get_id().clone(),
@ -3981,7 +3977,7 @@ impl ProfileCreateBridge for api::ProfileCreate {
.always_collect_billing_details_from_wallet_connector, .always_collect_billing_details_from_wallet_connector,
always_collect_shipping_details_from_wallet_connector: self always_collect_shipping_details_from_wallet_connector: self
.always_collect_shipping_details_from_wallet_connector, .always_collect_shipping_details_from_wallet_connector,
dynamic_routing_algorithm: None, dynamic_routing_algorithm: Some(dynamic_routing_algorithm),
is_network_tokenization_enabled: self.is_network_tokenization_enabled, is_network_tokenization_enabled: self.is_network_tokenization_enabled,
is_auto_retries_enabled: self.is_auto_retries_enabled.unwrap_or_default(), is_auto_retries_enabled: self.is_auto_retries_enabled.unwrap_or_default(),
max_auto_retries_enabled: self.max_auto_retries_enabled.map(i16::from), max_auto_retries_enabled: self.max_auto_retries_enabled.map(i16::from),
@ -4414,6 +4410,37 @@ impl ProfileUpdateBridge for api::ProfileUpdate {
} }
}; };
let dynamic_routing_algo_ref = if self.is_debit_routing_enabled == Some(true) {
let mut dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef =
business_profile
.dynamic_routing_algorithm
.clone()
.map(|val| val.parse_value("DynamicRoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to deserialize dynamic routing algorithm ref from business profile",
)?
.unwrap_or_default();
routing::helpers::create_merchant_in_decision_engine_if_not_exists(
state,
business_profile.get_id(),
&mut dynamic_routing_algo_ref,
)
.await;
let dynamic_routing_algo_ref_value = serde_json::to_value(dynamic_routing_algo_ref)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"error serializing dynamic_routing_algorithm_ref to JSON Value",
)?;
Some(dynamic_routing_algo_ref_value)
} else {
self.dynamic_routing_algorithm
};
Ok(domain::ProfileUpdate::Update(Box::new( Ok(domain::ProfileUpdate::Update(Box::new(
domain::ProfileGeneralUpdate { domain::ProfileGeneralUpdate {
profile_name: self.profile_name, profile_name: self.profile_name,
@ -4451,7 +4478,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate {
.always_collect_shipping_details_from_wallet_connector, .always_collect_shipping_details_from_wallet_connector,
tax_connector_id: self.tax_connector_id, tax_connector_id: self.tax_connector_id,
is_tax_connector_enabled: self.is_tax_connector_enabled, is_tax_connector_enabled: self.is_tax_connector_enabled,
dynamic_routing_algorithm: self.dynamic_routing_algorithm, dynamic_routing_algorithm: dynamic_routing_algo_ref,
is_network_tokenization_enabled: self.is_network_tokenization_enabled, is_network_tokenization_enabled: self.is_network_tokenization_enabled,
is_auto_retries_enabled: self.is_auto_retries_enabled, is_auto_retries_enabled: self.is_auto_retries_enabled,
max_auto_retries_enabled: self.max_auto_retries_enabled.map(i16::from), max_auto_retries_enabled: self.max_auto_retries_enabled.map(i16::from),

View File

@ -578,6 +578,7 @@ pub async fn link_routing_config(
business_profile.get_id(), business_profile.get_id(),
routing_algorithm.algorithm_data.clone(), routing_algorithm.algorithm_data.clone(),
routing_types::DynamicRoutingType::SuccessRateBasedRouting, routing_types::DynamicRoutingType::SuccessRateBasedRouting,
&mut dynamic_routing_ref,
) )
.await .await
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
@ -608,6 +609,7 @@ pub async fn link_routing_config(
business_profile.get_id(), business_profile.get_id(),
routing_algorithm.algorithm_data.clone(), routing_algorithm.algorithm_data.clone(),
routing_types::DynamicRoutingType::EliminationRouting, routing_types::DynamicRoutingType::EliminationRouting,
&mut dynamic_routing_ref,
) )
.await .await
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
@ -1929,6 +1931,9 @@ pub async fn contract_based_dynamic_routing_setup(
elimination_routing_algorithm: None, elimination_routing_algorithm: None,
dynamic_routing_volume_split: None, dynamic_routing_volume_split: None,
contract_based_routing: Some(contract_algo), contract_based_routing: Some(contract_algo),
is_merchant_created_in_decision_engine: dynamic_routing_algo_ref
.as_ref()
.is_some_and(|algo| algo.is_merchant_created_in_decision_engine),
} }
}; };

View File

@ -30,9 +30,9 @@ use external_services::grpc_client::dynamic_routing::{
use hyperswitch_domain_models::api::ApplicationResponse; use hyperswitch_domain_models::api::ApplicationResponse;
#[cfg(all(feature = "v1", feature = "dynamic_routing"))] #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
use hyperswitch_interfaces::events::routing_api_logs as routing_events; use hyperswitch_interfaces::events::routing_api_logs as routing_events;
#[cfg(all(feature = "dynamic_routing", feature = "v1"))] #[cfg(feature = "v1")]
use router_env::logger; use router_env::logger;
#[cfg(all(feature = "dynamic_routing", feature = "v1"))] #[cfg(feature = "v1")]
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use storage_impl::redis::cache; use storage_impl::redis::cache;
@ -52,15 +52,15 @@ use crate::{
types::{domain, storage}, types::{domain, storage},
utils::StringExt, utils::StringExt,
}; };
#[cfg(feature = "v1")]
use crate::{
core::payments::routing::utils::{self as routing_utils, DecisionEngineApiHandler},
services,
};
#[cfg(all(feature = "dynamic_routing", feature = "v1"))] #[cfg(all(feature = "dynamic_routing", feature = "v1"))]
use crate::{ use crate::{
core::{ core::{metrics as core_metrics, routing},
metrics as core_metrics,
payments::routing::utils::{self as routing_utils, DecisionEngineApiHandler},
routing,
},
routes::app::SessionStateInfo, routes::app::SessionStateInfo,
services,
types::transformers::ForeignInto, types::transformers::ForeignInto,
}; };
pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str = pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
@ -1723,7 +1723,7 @@ pub async fn disable_dynamic_routing_algorithm(
let db = state.store.as_ref(); let db = state.store.as_ref();
let key_manager_state = &state.into(); let key_manager_state = &state.into();
let profile_id = business_profile.get_id().clone(); let profile_id = business_profile.get_id().clone();
let (algorithm_id, dynamic_routing_algorithm, cache_entries_to_redact) = let (algorithm_id, mut dynamic_routing_algorithm, cache_entries_to_redact) =
match dynamic_routing_type { match dynamic_routing_type {
routing_types::DynamicRoutingType::SuccessRateBasedRouting => { routing_types::DynamicRoutingType::SuccessRateBasedRouting => {
let Some(algorithm_ref) = dynamic_routing_algo_ref.success_based_algorithm else { let Some(algorithm_ref) = dynamic_routing_algo_ref.success_based_algorithm else {
@ -1760,6 +1760,8 @@ pub async fn disable_dynamic_routing_algorithm(
contract_based_routing: dynamic_routing_algo_ref.contract_based_routing, contract_based_routing: dynamic_routing_algo_ref.contract_based_routing,
dynamic_routing_volume_split: dynamic_routing_algo_ref dynamic_routing_volume_split: dynamic_routing_algo_ref
.dynamic_routing_volume_split, .dynamic_routing_volume_split,
is_merchant_created_in_decision_engine: dynamic_routing_algo_ref
.is_merchant_created_in_decision_engine,
}, },
cache_entries_to_redact, cache_entries_to_redact,
) )
@ -1800,6 +1802,8 @@ pub async fn disable_dynamic_routing_algorithm(
}, },
), ),
contract_based_routing: dynamic_routing_algo_ref.contract_based_routing, contract_based_routing: dynamic_routing_algo_ref.contract_based_routing,
is_merchant_created_in_decision_engine: dynamic_routing_algo_ref
.is_merchant_created_in_decision_engine,
}, },
cache_entries_to_redact, cache_entries_to_redact,
) )
@ -1838,6 +1842,8 @@ pub async fn disable_dynamic_routing_algorithm(
routing_types::DynamicAlgorithmWithTimestamp::new(None), routing_types::DynamicAlgorithmWithTimestamp::new(None),
enabled_feature: routing_types::DynamicRoutingFeatures::None, enabled_feature: routing_types::DynamicRoutingFeatures::None,
}), }),
is_merchant_created_in_decision_engine: dynamic_routing_algo_ref
.is_merchant_created_in_decision_engine,
}, },
cache_entries_to_redact, cache_entries_to_redact,
) )
@ -1850,6 +1856,7 @@ pub async fn disable_dynamic_routing_algorithm(
state, state,
business_profile.get_id(), business_profile.get_id(),
dynamic_routing_type, dynamic_routing_type,
&mut dynamic_routing_algorithm,
) )
.await .await
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
@ -2088,6 +2095,7 @@ pub async fn default_specific_dynamic_routing_setup(
state, state,
business_profile.get_id(), business_profile.get_id(),
dynamic_routing_type, dynamic_routing_type,
&mut dynamic_routing_algo_ref,
) )
.await .await
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
@ -2203,6 +2211,7 @@ pub async fn enable_decision_engine_dynamic_routing_setup(
state: &SessionState, state: &SessionState,
profile_id: &id_type::ProfileId, profile_id: &id_type::ProfileId,
dynamic_routing_type: routing_types::DynamicRoutingType, dynamic_routing_type: routing_types::DynamicRoutingType,
dynamic_routing_algo_ref: &mut routing_types::DynamicRoutingAlgorithmRef,
) -> RouterResult<()> { ) -> RouterResult<()> {
logger::debug!( logger::debug!(
"performing call with open_router for profile {}", "performing call with open_router for profile {}",
@ -2248,6 +2257,10 @@ pub async fn enable_decision_engine_dynamic_routing_setup(
} }
}; };
// Create merchant in Decision Engine if it is not already created
create_merchant_in_decision_engine_if_not_exists(state, profile_id, dynamic_routing_algo_ref)
.await;
routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>( routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>(
state, state,
services::Method::Post, services::Method::Post,
@ -2269,6 +2282,7 @@ pub async fn update_decision_engine_dynamic_routing_setup(
profile_id: &id_type::ProfileId, profile_id: &id_type::ProfileId,
request: serde_json::Value, request: serde_json::Value,
dynamic_routing_type: routing_types::DynamicRoutingType, dynamic_routing_type: routing_types::DynamicRoutingType,
dynamic_routing_algo_ref: &mut routing_types::DynamicRoutingAlgorithmRef,
) -> RouterResult<()> { ) -> RouterResult<()> {
logger::debug!( logger::debug!(
"performing call with open_router for profile {}", "performing call with open_router for profile {}",
@ -2320,6 +2334,10 @@ pub async fn update_decision_engine_dynamic_routing_setup(
} }
}; };
// Create merchant in Decision Engine if it is not already created
create_merchant_in_decision_engine_if_not_exists(state, profile_id, dynamic_routing_algo_ref)
.await;
routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>( routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>(
state, state,
services::Method::Post, services::Method::Post,
@ -2340,6 +2358,7 @@ pub async fn disable_decision_engine_dynamic_routing_setup(
state: &SessionState, state: &SessionState,
profile_id: &id_type::ProfileId, profile_id: &id_type::ProfileId,
dynamic_routing_type: routing_types::DynamicRoutingType, dynamic_routing_type: routing_types::DynamicRoutingType,
dynamic_routing_algo_ref: &mut routing_types::DynamicRoutingAlgorithmRef,
) -> RouterResult<()> { ) -> RouterResult<()> {
logger::debug!( logger::debug!(
"performing call with open_router for profile {}", "performing call with open_router for profile {}",
@ -2364,6 +2383,10 @@ pub async fn disable_decision_engine_dynamic_routing_setup(
}, },
}; };
// Create merchant in Decision Engine if it is not already created
create_merchant_in_decision_engine_if_not_exists(state, profile_id, dynamic_routing_algo_ref)
.await;
routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>( routing_utils::ConfigApiClient::send_decision_engine_request::<_, String>(
state, state,
services::Method::Post, services::Method::Post,
@ -2378,7 +2401,32 @@ pub async fn disable_decision_engine_dynamic_routing_setup(
Ok(()) Ok(())
} }
#[cfg(all(feature = "dynamic_routing", feature = "v1"))] #[cfg(feature = "v1")]
#[instrument(skip_all)]
pub async fn create_merchant_in_decision_engine_if_not_exists(
state: &SessionState,
profile_id: &id_type::ProfileId,
dynamic_routing_algo_ref: &mut routing_types::DynamicRoutingAlgorithmRef,
) {
if !dynamic_routing_algo_ref.is_merchant_created_in_decision_engine {
logger::debug!(
"Creating merchant_account in decision engine for profile {}",
profile_id.get_string_repr()
);
create_decision_engine_merchant(state, profile_id)
.await
.map_err(|err| {
logger::warn!("Merchant creation error in decision_engine: {err:?}");
})
.ok();
// TODO: Update the status based on the status code or error message from the API call
dynamic_routing_algo_ref.update_merchant_creation_status_in_decision_engine(true);
}
}
#[cfg(feature = "v1")]
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn create_decision_engine_merchant( pub async fn create_decision_engine_merchant(
state: &SessionState, state: &SessionState,