mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1672 lines
65 KiB
Rust
1672 lines
65 KiB
Rust
//! Analysis for usage of all helper functions for use case of routing
|
|
//!
|
|
//! Functions that are used to perform the retrieval of merchant's
|
|
//! routing dict, configs, defaults
|
|
use std::fmt::Debug;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use std::str::FromStr;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use std::sync::Arc;
|
|
|
|
use api_models::routing as routing_types;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use common_utils::ext_traits::ValueExt;
|
|
use common_utils::{ext_traits::Encode, id_type, types::keymanager::KeyManagerState};
|
|
use diesel_models::configs;
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
use diesel_models::dynamic_routing_stats::DynamicRoutingStatsNew;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use diesel_models::routing_algorithm;
|
|
use error_stack::ResultExt;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use external_services::grpc_client::dynamic_routing::{
|
|
contract_routing_client::ContractBasedDynamicRouting,
|
|
success_rate_client::SuccessBasedDynamicRouting,
|
|
};
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use hyperswitch_domain_models::api::ApplicationResponse;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use router_env::logger;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use router_env::{instrument, tracing};
|
|
use rustc_hash::FxHashSet;
|
|
use storage_impl::redis::cache;
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use storage_impl::redis::cache::Cacheable;
|
|
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use crate::db::errors::StorageErrorExt;
|
|
#[cfg(feature = "v2")]
|
|
use crate::types::domain::MerchantConnectorAccount;
|
|
use crate::{
|
|
core::errors::{self, RouterResult},
|
|
db::StorageInterface,
|
|
routes::SessionState,
|
|
types::{domain, storage},
|
|
utils::StringExt,
|
|
};
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
use crate::{core::metrics as core_metrics, types::transformers::ForeignInto};
|
|
pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
|
|
"Success rate based dynamic routing algorithm";
|
|
pub const ELIMINATION_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
|
|
"Elimination based dynamic routing algorithm";
|
|
pub const CONTRACT_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
|
|
"Contract based dynamic routing algorithm";
|
|
|
|
/// Provides us with all the configured configs of the Merchant in the ascending time configured
|
|
/// manner and chooses the first of them
|
|
pub async fn get_merchant_default_config(
|
|
db: &dyn StorageInterface,
|
|
// Cannot make this as merchant id domain type because, we are passing profile id also here
|
|
merchant_id: &str,
|
|
transaction_type: &storage::enums::TransactionType,
|
|
) -> RouterResult<Vec<routing_types::RoutableConnectorChoice>> {
|
|
let key = get_default_config_key(merchant_id, transaction_type);
|
|
let maybe_config = db.find_config_by_key(&key).await;
|
|
|
|
match maybe_config {
|
|
Ok(config) => config
|
|
.config
|
|
.parse_struct("Vec<RoutableConnectors>")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Merchant default config has invalid structure"),
|
|
|
|
Err(e) if e.current_context().is_db_not_found() => {
|
|
let new_config_conns = Vec::<routing_types::RoutableConnectorChoice>::new();
|
|
let serialized = new_config_conns
|
|
.encode_to_string_of_json()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"Error while creating and serializing new merchant default config",
|
|
)?;
|
|
|
|
let new_config = configs::ConfigNew {
|
|
key,
|
|
config: serialized,
|
|
};
|
|
|
|
db.insert_config(new_config)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error inserting new default routing config into DB")?;
|
|
|
|
Ok(new_config_conns)
|
|
}
|
|
|
|
Err(e) => Err(e)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error fetching default config for merchant"),
|
|
}
|
|
}
|
|
|
|
/// Merchant's already created config can be updated and this change will be reflected
|
|
/// in DB as well for the particular updated config
|
|
pub async fn update_merchant_default_config(
|
|
db: &dyn StorageInterface,
|
|
merchant_id: &str,
|
|
connectors: Vec<routing_types::RoutableConnectorChoice>,
|
|
transaction_type: &storage::enums::TransactionType,
|
|
) -> RouterResult<()> {
|
|
let key = get_default_config_key(merchant_id, transaction_type);
|
|
let config_str = connectors
|
|
.encode_to_string_of_json()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to serialize merchant default routing config during update")?;
|
|
|
|
let config_update = configs::ConfigUpdate::Update {
|
|
config: Some(config_str),
|
|
};
|
|
|
|
db.update_config_by_key(&key, config_update)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error updating the default routing config in DB")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_merchant_routing_dictionary(
|
|
db: &dyn StorageInterface,
|
|
merchant_id: &str,
|
|
dictionary: routing_types::RoutingDictionary,
|
|
) -> RouterResult<()> {
|
|
let key = get_routing_dictionary_key(merchant_id);
|
|
let dictionary_str = dictionary
|
|
.encode_to_string_of_json()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to serialize routing dictionary during update")?;
|
|
|
|
let config_update = configs::ConfigUpdate::Update {
|
|
config: Some(dictionary_str),
|
|
};
|
|
|
|
db.update_config_by_key(&key, config_update)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error saving routing dictionary to DB")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// This will help make one of all configured algorithms to be in active state for a particular
|
|
/// merchant
|
|
#[cfg(feature = "v1")]
|
|
pub async fn update_merchant_active_algorithm_ref(
|
|
state: &SessionState,
|
|
key_store: &domain::MerchantKeyStore,
|
|
config_key: cache::CacheKind<'_>,
|
|
algorithm_id: routing_types::RoutingAlgorithmRef,
|
|
) -> RouterResult<()> {
|
|
let ref_value = algorithm_id
|
|
.encode_to_value()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed converting routing algorithm ref to json value")?;
|
|
|
|
let merchant_account_update = storage::MerchantAccountUpdate::Update {
|
|
merchant_name: None,
|
|
merchant_details: None,
|
|
return_url: None,
|
|
webhook_details: None,
|
|
sub_merchants_enabled: None,
|
|
parent_merchant_id: None,
|
|
enable_payment_response_hash: None,
|
|
payment_response_hash_key: None,
|
|
redirect_to_merchant_with_http_post: None,
|
|
publishable_key: None,
|
|
locker_id: None,
|
|
metadata: None,
|
|
routing_algorithm: Some(ref_value),
|
|
primary_business_details: None,
|
|
intent_fulfillment_time: None,
|
|
frm_routing_algorithm: None,
|
|
payout_routing_algorithm: None,
|
|
default_profile: None,
|
|
payment_link_config: None,
|
|
pm_collect_link_config: None,
|
|
};
|
|
|
|
let db = &*state.store;
|
|
db.update_specific_fields_in_merchant(
|
|
&state.into(),
|
|
&key_store.merchant_id,
|
|
merchant_account_update,
|
|
key_store,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to update routing algorithm ref in merchant account")?;
|
|
|
|
cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [config_key])
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to invalidate the config cache")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub async fn update_profile_active_algorithm_ref(
|
|
db: &dyn StorageInterface,
|
|
key_manager_state: &KeyManagerState,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
current_business_profile: domain::Profile,
|
|
algorithm_id: routing_types::RoutingAlgorithmRef,
|
|
transaction_type: &storage::enums::TransactionType,
|
|
) -> RouterResult<()> {
|
|
let ref_val = algorithm_id
|
|
.encode_to_value()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to convert routing ref to value")?;
|
|
|
|
let merchant_id = current_business_profile.merchant_id.clone();
|
|
|
|
let profile_id = current_business_profile.get_id().to_owned();
|
|
|
|
let routing_cache_key = cache::CacheKind::Routing(
|
|
format!(
|
|
"routing_config_{}_{}",
|
|
merchant_id.get_string_repr(),
|
|
profile_id.get_string_repr(),
|
|
)
|
|
.into(),
|
|
);
|
|
|
|
let (routing_algorithm, payout_routing_algorithm) = match transaction_type {
|
|
storage::enums::TransactionType::Payment => (Some(ref_val), None),
|
|
#[cfg(feature = "payouts")]
|
|
storage::enums::TransactionType::Payout => (None, Some(ref_val)),
|
|
};
|
|
|
|
let business_profile_update = domain::ProfileUpdate::RoutingAlgorithmUpdate {
|
|
routing_algorithm,
|
|
payout_routing_algorithm,
|
|
};
|
|
|
|
db.update_profile_by_profile_id(
|
|
key_manager_state,
|
|
merchant_key_store,
|
|
current_business_profile,
|
|
business_profile_update,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to update routing algorithm ref in business profile")?;
|
|
|
|
cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [routing_cache_key])
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to invalidate routing cache")?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub async fn update_business_profile_active_dynamic_algorithm_ref(
|
|
db: &dyn StorageInterface,
|
|
key_manager_state: &KeyManagerState,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
current_business_profile: domain::Profile,
|
|
dynamic_routing_algorithm_ref: routing_types::DynamicRoutingAlgorithmRef,
|
|
) -> RouterResult<()> {
|
|
let ref_val = dynamic_routing_algorithm_ref
|
|
.encode_to_value()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to convert dynamic routing ref to value")?;
|
|
let business_profile_update = domain::ProfileUpdate::DynamicRoutingAlgorithmUpdate {
|
|
dynamic_routing_algorithm: Some(ref_val),
|
|
};
|
|
db.update_profile_by_profile_id(
|
|
key_manager_state,
|
|
merchant_key_store,
|
|
current_business_profile,
|
|
business_profile_update,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to update dynamic routing algorithm ref in business profile")?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[derive(Clone, Debug)]
|
|
pub struct RoutingAlgorithmHelpers<'h> {
|
|
pub name_mca_id_set: ConnectNameAndMCAIdForProfile<'h>,
|
|
pub name_set: ConnectNameForProfile<'h>,
|
|
pub routing_algorithm: &'h routing_types::RoutingAlgorithm,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ConnectNameAndMCAIdForProfile<'a>(
|
|
pub FxHashSet<(
|
|
&'a common_enums::connector_enums::Connector,
|
|
id_type::MerchantConnectorAccountId,
|
|
)>,
|
|
);
|
|
#[derive(Clone, Debug)]
|
|
pub struct ConnectNameForProfile<'a>(pub FxHashSet<&'a common_enums::connector_enums::Connector>);
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[derive(Clone, Debug)]
|
|
pub struct MerchantConnectorAccounts(pub Vec<MerchantConnectorAccount>);
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl MerchantConnectorAccounts {
|
|
pub async fn get_all_mcas(
|
|
merchant_id: &id_type::MerchantId,
|
|
key_store: &domain::MerchantKeyStore,
|
|
state: &SessionState,
|
|
) -> RouterResult<Self> {
|
|
let db = &*state.store;
|
|
let key_manager_state = &state.into();
|
|
Ok(Self(
|
|
db.find_merchant_connector_account_by_merchant_id_and_disabled_list(
|
|
key_manager_state,
|
|
merchant_id,
|
|
true,
|
|
key_store,
|
|
)
|
|
.await
|
|
.change_context(
|
|
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: merchant_id.get_string_repr().to_owned(),
|
|
},
|
|
)?,
|
|
))
|
|
}
|
|
|
|
fn filter_and_map<'a, T>(
|
|
&'a self,
|
|
filter: impl Fn(&'a MerchantConnectorAccount) -> bool,
|
|
func: impl Fn(&'a MerchantConnectorAccount) -> T,
|
|
) -> FxHashSet<T>
|
|
where
|
|
T: std::hash::Hash + Eq,
|
|
{
|
|
self.0
|
|
.iter()
|
|
.filter(|mca| filter(mca))
|
|
.map(func)
|
|
.collect::<FxHashSet<_>>()
|
|
}
|
|
|
|
pub fn filter_by_profile<'a, T>(
|
|
&'a self,
|
|
profile_id: &'a id_type::ProfileId,
|
|
func: impl Fn(&'a MerchantConnectorAccount) -> T,
|
|
) -> FxHashSet<T>
|
|
where
|
|
T: std::hash::Hash + Eq,
|
|
{
|
|
self.filter_and_map(|mca| mca.profile_id == *profile_id, func)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl RoutingAlgorithmHelpers<'_> {
|
|
fn connector_choice(
|
|
&self,
|
|
choice: &routing_types::RoutableConnectorChoice,
|
|
) -> RouterResult<()> {
|
|
if let Some(ref mca_id) = choice.merchant_connector_id {
|
|
let connector_choice = common_enums::connector_enums::Connector::from(choice.connector);
|
|
error_stack::ensure!(
|
|
self.name_mca_id_set.0.contains(&(&connector_choice, mca_id.clone())),
|
|
errors::ApiErrorResponse::InvalidRequestData {
|
|
message: format!(
|
|
"connector with name '{}' and merchant connector account id '{:?}' not found for the given profile",
|
|
connector_choice,
|
|
mca_id,
|
|
)
|
|
}
|
|
);
|
|
} else {
|
|
let connector_choice = common_enums::connector_enums::Connector::from(choice.connector);
|
|
error_stack::ensure!(
|
|
self.name_set.0.contains(&connector_choice),
|
|
errors::ApiErrorResponse::InvalidRequestData {
|
|
message: format!(
|
|
"connector with name '{}' not found for the given profile",
|
|
connector_choice,
|
|
)
|
|
}
|
|
);
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
pub fn validate_connectors_in_routing_config(&self) -> RouterResult<()> {
|
|
match self.routing_algorithm {
|
|
routing_types::RoutingAlgorithm::Single(choice) => {
|
|
self.connector_choice(choice)?;
|
|
}
|
|
|
|
routing_types::RoutingAlgorithm::Priority(list) => {
|
|
for choice in list {
|
|
self.connector_choice(choice)?;
|
|
}
|
|
}
|
|
|
|
routing_types::RoutingAlgorithm::VolumeSplit(splits) => {
|
|
for split in splits {
|
|
self.connector_choice(&split.connector)?;
|
|
}
|
|
}
|
|
|
|
routing_types::RoutingAlgorithm::Advanced(program) => {
|
|
let check_connector_selection =
|
|
|selection: &routing_types::ConnectorSelection| -> RouterResult<()> {
|
|
match selection {
|
|
routing_types::ConnectorSelection::VolumeSplit(splits) => {
|
|
for split in splits {
|
|
self.connector_choice(&split.connector)?;
|
|
}
|
|
}
|
|
|
|
routing_types::ConnectorSelection::Priority(list) => {
|
|
for choice in list {
|
|
self.connector_choice(choice)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
};
|
|
|
|
check_connector_selection(&program.default_selection)?;
|
|
|
|
for rule in &program.rules {
|
|
check_connector_selection(&rule.connector_selection)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub async fn validate_connectors_in_routing_config(
|
|
state: &SessionState,
|
|
key_store: &domain::MerchantKeyStore,
|
|
merchant_id: &id_type::MerchantId,
|
|
profile_id: &id_type::ProfileId,
|
|
routing_algorithm: &routing_types::RoutingAlgorithm,
|
|
) -> RouterResult<()> {
|
|
let all_mcas = &*state
|
|
.store
|
|
.find_merchant_connector_account_by_merchant_id_and_disabled_list(
|
|
&state.into(),
|
|
merchant_id,
|
|
true,
|
|
key_store,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: merchant_id.get_string_repr().to_owned(),
|
|
})?;
|
|
let name_mca_id_set = all_mcas
|
|
.iter()
|
|
.filter(|mca| mca.profile_id == *profile_id)
|
|
.map(|mca| (&mca.connector_name, mca.get_id()))
|
|
.collect::<FxHashSet<_>>();
|
|
|
|
let name_set = all_mcas
|
|
.iter()
|
|
.filter(|mca| mca.profile_id == *profile_id)
|
|
.map(|mca| &mca.connector_name)
|
|
.collect::<FxHashSet<_>>();
|
|
|
|
let connector_choice = |choice: &routing_types::RoutableConnectorChoice| {
|
|
if let Some(ref mca_id) = choice.merchant_connector_id {
|
|
error_stack::ensure!(
|
|
name_mca_id_set.contains(&(&choice.connector.to_string(), mca_id.clone())),
|
|
errors::ApiErrorResponse::InvalidRequestData {
|
|
message: format!(
|
|
"connector with name '{}' and merchant connector account id '{:?}' not found for the given profile",
|
|
choice.connector,
|
|
mca_id,
|
|
)
|
|
}
|
|
);
|
|
} else {
|
|
error_stack::ensure!(
|
|
name_set.contains(&choice.connector.to_string()),
|
|
errors::ApiErrorResponse::InvalidRequestData {
|
|
message: format!(
|
|
"connector with name '{}' not found for the given profile",
|
|
choice.connector,
|
|
)
|
|
}
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
};
|
|
|
|
match routing_algorithm {
|
|
routing_types::RoutingAlgorithm::Single(choice) => {
|
|
connector_choice(choice)?;
|
|
}
|
|
|
|
routing_types::RoutingAlgorithm::Priority(list) => {
|
|
for choice in list {
|
|
connector_choice(choice)?;
|
|
}
|
|
}
|
|
|
|
routing_types::RoutingAlgorithm::VolumeSplit(splits) => {
|
|
for split in splits {
|
|
connector_choice(&split.connector)?;
|
|
}
|
|
}
|
|
|
|
routing_types::RoutingAlgorithm::Advanced(program) => {
|
|
let check_connector_selection =
|
|
|selection: &routing_types::ConnectorSelection| -> RouterResult<()> {
|
|
match selection {
|
|
routing_types::ConnectorSelection::VolumeSplit(splits) => {
|
|
for split in splits {
|
|
connector_choice(&split.connector)?;
|
|
}
|
|
}
|
|
|
|
routing_types::ConnectorSelection::Priority(list) => {
|
|
for choice in list {
|
|
connector_choice(choice)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
};
|
|
|
|
check_connector_selection(&program.default_selection)?;
|
|
|
|
for rule in &program.rules {
|
|
check_connector_selection(&rule.connector_selection)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Provides the identifier for the specific merchant's routing_dictionary_key
|
|
#[inline(always)]
|
|
pub fn get_routing_dictionary_key(merchant_id: &str) -> String {
|
|
format!("routing_dict_{merchant_id}")
|
|
}
|
|
|
|
/// Provides the identifier for the specific merchant's default_config
|
|
#[inline(always)]
|
|
pub fn get_default_config_key(
|
|
merchant_id: &str,
|
|
transaction_type: &storage::enums::TransactionType,
|
|
) -> String {
|
|
match transaction_type {
|
|
storage::enums::TransactionType::Payment => format!("routing_default_{merchant_id}"),
|
|
#[cfg(feature = "payouts")]
|
|
storage::enums::TransactionType::Payout => format!("routing_default_po_{merchant_id}"),
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
#[async_trait::async_trait]
|
|
pub trait DynamicRoutingCache {
|
|
async fn get_cached_dynamic_routing_config_for_profile(
|
|
state: &SessionState,
|
|
key: &str,
|
|
) -> Option<Arc<Self>>;
|
|
|
|
async fn refresh_dynamic_routing_cache<T, F, Fut>(
|
|
state: &SessionState,
|
|
key: &str,
|
|
func: F,
|
|
) -> RouterResult<T>
|
|
where
|
|
F: FnOnce() -> Fut + Send,
|
|
T: Cacheable + serde::Serialize + serde::de::DeserializeOwned + Debug + Clone,
|
|
Fut: futures::Future<Output = errors::CustomResult<T, errors::StorageError>> + Send;
|
|
}
|
|
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
#[async_trait::async_trait]
|
|
impl DynamicRoutingCache for routing_types::SuccessBasedRoutingConfig {
|
|
async fn get_cached_dynamic_routing_config_for_profile(
|
|
state: &SessionState,
|
|
key: &str,
|
|
) -> Option<Arc<Self>> {
|
|
cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE
|
|
.get_val::<Arc<Self>>(cache::CacheKey {
|
|
key: key.to_string(),
|
|
prefix: state.tenant.redis_key_prefix.clone(),
|
|
})
|
|
.await
|
|
}
|
|
|
|
async fn refresh_dynamic_routing_cache<T, F, Fut>(
|
|
state: &SessionState,
|
|
key: &str,
|
|
func: F,
|
|
) -> RouterResult<T>
|
|
where
|
|
F: FnOnce() -> Fut + Send,
|
|
T: Cacheable + serde::Serialize + serde::de::DeserializeOwned + Debug + Clone,
|
|
Fut: futures::Future<Output = errors::CustomResult<T, errors::StorageError>> + Send,
|
|
{
|
|
cache::get_or_populate_in_memory(
|
|
state.store.get_cache_store().as_ref(),
|
|
key,
|
|
func,
|
|
&cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("unable to populate SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE")
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
#[async_trait::async_trait]
|
|
impl DynamicRoutingCache for routing_types::ContractBasedRoutingConfig {
|
|
async fn get_cached_dynamic_routing_config_for_profile(
|
|
state: &SessionState,
|
|
key: &str,
|
|
) -> Option<Arc<Self>> {
|
|
cache::CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE
|
|
.get_val::<Arc<Self>>(cache::CacheKey {
|
|
key: key.to_string(),
|
|
prefix: state.tenant.redis_key_prefix.clone(),
|
|
})
|
|
.await
|
|
}
|
|
|
|
async fn refresh_dynamic_routing_cache<T, F, Fut>(
|
|
state: &SessionState,
|
|
key: &str,
|
|
func: F,
|
|
) -> RouterResult<T>
|
|
where
|
|
F: FnOnce() -> Fut + Send,
|
|
T: Cacheable + serde::Serialize + serde::de::DeserializeOwned + Debug + Clone,
|
|
Fut: futures::Future<Output = errors::CustomResult<T, errors::StorageError>> + Send,
|
|
{
|
|
cache::get_or_populate_in_memory(
|
|
state.store.get_cache_store().as_ref(),
|
|
key,
|
|
func,
|
|
&cache::CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("unable to populate CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE")
|
|
}
|
|
}
|
|
|
|
/// Cfetch dynamic routing configs
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
#[instrument(skip_all)]
|
|
pub async fn fetch_dynamic_routing_configs<T>(
|
|
state: &SessionState,
|
|
profile_id: &id_type::ProfileId,
|
|
routing_id: id_type::RoutingId,
|
|
) -> RouterResult<T>
|
|
where
|
|
T: serde::de::DeserializeOwned
|
|
+ Clone
|
|
+ DynamicRoutingCache
|
|
+ Cacheable
|
|
+ serde::Serialize
|
|
+ Debug,
|
|
{
|
|
let key = format!(
|
|
"{}_{}",
|
|
profile_id.get_string_repr(),
|
|
routing_id.get_string_repr()
|
|
);
|
|
|
|
if let Some(config) =
|
|
T::get_cached_dynamic_routing_config_for_profile(state, key.as_str()).await
|
|
{
|
|
Ok(config.as_ref().clone())
|
|
} else {
|
|
let func = || async {
|
|
let routing_algorithm = state
|
|
.store
|
|
.find_routing_algorithm_by_profile_id_algorithm_id(profile_id, &routing_id)
|
|
.await
|
|
.change_context(errors::StorageError::ValueNotFound(
|
|
"RoutingAlgorithm".to_string(),
|
|
))
|
|
.attach_printable("unable to retrieve routing_algorithm for profile from db")?;
|
|
|
|
let dynamic_routing_config = routing_algorithm
|
|
.algorithm_data
|
|
.parse_value::<T>("dynamic_routing_config")
|
|
.change_context(errors::StorageError::DeserializationFailed)
|
|
.attach_printable("unable to parse dynamic_routing_config")?;
|
|
|
|
Ok(dynamic_routing_config)
|
|
};
|
|
|
|
let dynamic_routing_config =
|
|
T::refresh_dynamic_routing_cache(state, key.as_str(), func).await?;
|
|
|
|
Ok(dynamic_routing_config)
|
|
}
|
|
}
|
|
|
|
/// metrics for success based dynamic routing
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
#[instrument(skip_all)]
|
|
pub async fn push_metrics_with_update_window_for_success_based_routing(
|
|
state: &SessionState,
|
|
payment_attempt: &storage::PaymentAttempt,
|
|
routable_connectors: Vec<routing_types::RoutableConnectorChoice>,
|
|
profile_id: &id_type::ProfileId,
|
|
dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
|
|
dynamic_routing_config_params_interpolator: DynamicRoutingConfigParamsInterpolator,
|
|
) -> RouterResult<()> {
|
|
let success_based_algo_ref = dynamic_routing_algo_ref
|
|
.success_based_algorithm
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("success_based_algorithm not found in dynamic_routing_algorithm from business_profile table")?;
|
|
|
|
if success_based_algo_ref.enabled_feature != routing_types::DynamicRoutingFeatures::None {
|
|
let client = state
|
|
.grpc_client
|
|
.dynamic_routing
|
|
.success_rate_client
|
|
.as_ref()
|
|
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
|
message: "success_rate gRPC client not found".to_string(),
|
|
})?;
|
|
|
|
let payment_connector = &payment_attempt.connector.clone().ok_or(
|
|
errors::ApiErrorResponse::GenericNotFoundError {
|
|
message: "unable to derive payment connector from payment attempt".to_string(),
|
|
},
|
|
)?;
|
|
|
|
let success_based_routing_configs =
|
|
fetch_dynamic_routing_configs::<routing_types::SuccessBasedRoutingConfig>(
|
|
state,
|
|
profile_id,
|
|
success_based_algo_ref
|
|
.algorithm_id_with_timestamp
|
|
.algorithm_id
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"success_based_routing_algorithm_id not found in business_profile",
|
|
)?,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("unable to retrieve success_rate based dynamic routing configs")?;
|
|
|
|
let success_based_routing_config_params = dynamic_routing_config_params_interpolator
|
|
.get_string_val(
|
|
success_based_routing_configs
|
|
.params
|
|
.as_ref()
|
|
.ok_or(errors::RoutingError::SuccessBasedRoutingParamsNotFoundError)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)?,
|
|
);
|
|
|
|
let success_based_connectors = client
|
|
.calculate_entity_and_global_success_rate(
|
|
profile_id.get_string_repr().into(),
|
|
success_based_routing_configs.clone(),
|
|
success_based_routing_config_params.clone(),
|
|
routable_connectors.clone(),
|
|
state.get_grpc_headers(),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to calculate/fetch success rate from dynamic routing service",
|
|
)?;
|
|
|
|
let payment_status_attribute =
|
|
get_desired_payment_status_for_dynamic_routing_metrics(payment_attempt.status);
|
|
|
|
let first_merchant_success_based_connector = &success_based_connectors
|
|
.entity_scores_with_labels
|
|
.first()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
|
|
)?;
|
|
|
|
let (first_merchant_success_based_connector_label, _) = first_merchant_success_based_connector.label
|
|
.split_once(':')
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(format!(
|
|
"unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service",
|
|
first_merchant_success_based_connector.label
|
|
))?;
|
|
|
|
let first_global_success_based_connector = &success_based_connectors
|
|
.global_scores_with_labels
|
|
.first()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to fetch the first global connector from list of connectors obtained from dynamic routing service",
|
|
)?;
|
|
|
|
let outcome = get_dynamic_routing_based_metrics_outcome_for_payment(
|
|
payment_status_attribute,
|
|
payment_connector.to_string(),
|
|
first_merchant_success_based_connector_label.to_string(),
|
|
);
|
|
|
|
let dynamic_routing_stats = DynamicRoutingStatsNew {
|
|
payment_id: payment_attempt.payment_id.to_owned(),
|
|
attempt_id: payment_attempt.attempt_id.clone(),
|
|
merchant_id: payment_attempt.merchant_id.to_owned(),
|
|
profile_id: payment_attempt.profile_id.to_owned(),
|
|
amount: payment_attempt.get_total_amount(),
|
|
success_based_routing_connector: first_merchant_success_based_connector_label
|
|
.to_string(),
|
|
payment_connector: payment_connector.to_string(),
|
|
payment_method_type: payment_attempt.payment_method_type,
|
|
currency: payment_attempt.currency,
|
|
payment_method: payment_attempt.payment_method,
|
|
capture_method: payment_attempt.capture_method,
|
|
authentication_type: payment_attempt.authentication_type,
|
|
payment_status: payment_attempt.status,
|
|
conclusive_classification: outcome,
|
|
created_at: common_utils::date_time::now(),
|
|
global_success_based_connector: Some(
|
|
first_global_success_based_connector.label.to_string(),
|
|
),
|
|
};
|
|
|
|
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
|
|
1,
|
|
router_env::metric_attributes!(
|
|
(
|
|
"tenant",
|
|
state.tenant.tenant_id.get_string_repr().to_owned(),
|
|
),
|
|
(
|
|
"merchant_profile_id",
|
|
format!(
|
|
"{}:{}",
|
|
payment_attempt.merchant_id.get_string_repr(),
|
|
payment_attempt.profile_id.get_string_repr()
|
|
),
|
|
),
|
|
(
|
|
"merchant_specific_success_based_routing_connector",
|
|
first_merchant_success_based_connector_label.to_string(),
|
|
),
|
|
(
|
|
"merchant_specific_success_based_routing_connector_score",
|
|
first_merchant_success_based_connector.score.to_string(),
|
|
),
|
|
(
|
|
"global_success_based_routing_connector",
|
|
first_global_success_based_connector.label.to_string(),
|
|
),
|
|
(
|
|
"global_success_based_routing_connector_score",
|
|
first_global_success_based_connector.score.to_string(),
|
|
),
|
|
("payment_connector", payment_connector.to_string()),
|
|
(
|
|
"currency",
|
|
payment_attempt
|
|
.currency
|
|
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
|
|
),
|
|
(
|
|
"payment_method",
|
|
payment_attempt.payment_method.map_or_else(
|
|
|| "None".to_string(),
|
|
|payment_method| payment_method.to_string(),
|
|
),
|
|
),
|
|
(
|
|
"payment_method_type",
|
|
payment_attempt.payment_method_type.map_or_else(
|
|
|| "None".to_string(),
|
|
|payment_method_type| payment_method_type.to_string(),
|
|
),
|
|
),
|
|
(
|
|
"capture_method",
|
|
payment_attempt.capture_method.map_or_else(
|
|
|| "None".to_string(),
|
|
|capture_method| capture_method.to_string(),
|
|
),
|
|
),
|
|
(
|
|
"authentication_type",
|
|
payment_attempt.authentication_type.map_or_else(
|
|
|| "None".to_string(),
|
|
|authentication_type| authentication_type.to_string(),
|
|
),
|
|
),
|
|
("payment_status", payment_attempt.status.to_string()),
|
|
("conclusive_classification", outcome.to_string()),
|
|
),
|
|
);
|
|
logger::debug!("successfully pushed success_based_routing metrics");
|
|
|
|
state
|
|
.store
|
|
.insert_dynamic_routing_stat_entry(dynamic_routing_stats)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to push dynamic routing stats to db")?;
|
|
|
|
client
|
|
.update_success_rate(
|
|
profile_id.get_string_repr().into(),
|
|
success_based_routing_configs,
|
|
success_based_routing_config_params,
|
|
vec![routing_types::RoutableConnectorChoiceWithStatus::new(
|
|
routing_types::RoutableConnectorChoice {
|
|
choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
|
|
connector: common_enums::RoutableConnectors::from_str(
|
|
payment_connector.as_str(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("unable to infer routable_connector from connector")?,
|
|
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
|
|
},
|
|
payment_status_attribute == common_enums::AttemptStatus::Charged,
|
|
)],
|
|
state.get_grpc_headers(),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to update success based routing window in dynamic routing service",
|
|
)?;
|
|
Ok(())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// metrics for contract based dynamic routing
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
#[instrument(skip_all)]
|
|
pub async fn push_metrics_with_update_window_for_contract_based_routing(
|
|
state: &SessionState,
|
|
payment_attempt: &storage::PaymentAttempt,
|
|
routable_connectors: Vec<routing_types::RoutableConnectorChoice>,
|
|
profile_id: &id_type::ProfileId,
|
|
dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
|
|
_dynamic_routing_config_params_interpolator: DynamicRoutingConfigParamsInterpolator,
|
|
) -> RouterResult<()> {
|
|
let contract_routing_algo_ref = dynamic_routing_algo_ref
|
|
.contract_based_routing
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("contract_routing_algorithm not found in dynamic_routing_algorithm from business_profile table")?;
|
|
|
|
if contract_routing_algo_ref.enabled_feature != routing_types::DynamicRoutingFeatures::None {
|
|
let client = state
|
|
.grpc_client
|
|
.dynamic_routing
|
|
.contract_based_client
|
|
.clone()
|
|
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
|
message: "contract_routing gRPC client not found".to_string(),
|
|
})?;
|
|
|
|
let payment_connector = &payment_attempt.connector.clone().ok_or(
|
|
errors::ApiErrorResponse::GenericNotFoundError {
|
|
message: "unable to derive payment connector from payment attempt".to_string(),
|
|
},
|
|
)?;
|
|
|
|
let contract_based_routing_config =
|
|
fetch_dynamic_routing_configs::<routing_types::ContractBasedRoutingConfig>(
|
|
state,
|
|
profile_id,
|
|
contract_routing_algo_ref
|
|
.algorithm_id_with_timestamp
|
|
.algorithm_id
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"contract_based_routing_algorithm_id not found in business_profile",
|
|
)?,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("unable to retrieve contract based dynamic routing configs")?;
|
|
|
|
let mut existing_label_info = None;
|
|
|
|
contract_based_routing_config
|
|
.label_info
|
|
.as_ref()
|
|
.map(|label_info_vec| {
|
|
for label_info in label_info_vec {
|
|
if Some(&label_info.mca_id) == payment_attempt.merchant_connector_id.as_ref() {
|
|
existing_label_info = Some(label_info.clone());
|
|
}
|
|
}
|
|
});
|
|
|
|
let final_label_info = existing_label_info
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("unable to get LabelInformation from ContractBasedRoutingConfig")?;
|
|
|
|
logger::debug!(
|
|
"contract based routing: matched LabelInformation - {:?}",
|
|
final_label_info
|
|
);
|
|
|
|
let request_label_info = routing_types::LabelInformation {
|
|
label: format!(
|
|
"{}:{}",
|
|
final_label_info.label.clone(),
|
|
final_label_info.mca_id.get_string_repr()
|
|
),
|
|
target_count: final_label_info.target_count,
|
|
target_time: final_label_info.target_time,
|
|
mca_id: final_label_info.mca_id.to_owned(),
|
|
};
|
|
|
|
let payment_status_attribute =
|
|
get_desired_payment_status_for_dynamic_routing_metrics(payment_attempt.status);
|
|
|
|
if payment_status_attribute == common_enums::AttemptStatus::Charged {
|
|
client
|
|
.update_contracts(
|
|
profile_id.get_string_repr().into(),
|
|
vec![request_label_info],
|
|
"".to_string(),
|
|
vec![],
|
|
state.get_grpc_headers(),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to update contract based routing window in dynamic routing service",
|
|
)?;
|
|
}
|
|
|
|
let contract_scores = client
|
|
.calculate_contract_score(
|
|
profile_id.get_string_repr().into(),
|
|
contract_based_routing_config.clone(),
|
|
"".to_string(),
|
|
routable_connectors.clone(),
|
|
state.get_grpc_headers(),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to calculate/fetch contract scores from dynamic routing service",
|
|
)?;
|
|
|
|
let first_contract_based_connector = &contract_scores
|
|
.labels_with_score
|
|
.first()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
|
|
)?;
|
|
|
|
let (first_contract_based_connector, connector_score, current_payment_cnt) = (first_contract_based_connector.label
|
|
.split_once(':')
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(format!(
|
|
"unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service",
|
|
first_contract_based_connector
|
|
))?
|
|
.0, first_contract_based_connector.score, first_contract_based_connector.current_count );
|
|
|
|
core_metrics::DYNAMIC_CONTRACT_BASED_ROUTING.add(
|
|
1,
|
|
router_env::metric_attributes!(
|
|
(
|
|
"tenant",
|
|
state.tenant.tenant_id.get_string_repr().to_owned(),
|
|
),
|
|
(
|
|
"merchant_profile_id",
|
|
format!(
|
|
"{}:{}",
|
|
payment_attempt.merchant_id.get_string_repr(),
|
|
payment_attempt.profile_id.get_string_repr()
|
|
),
|
|
),
|
|
(
|
|
"contract_based_routing_connector",
|
|
first_contract_based_connector.to_string(),
|
|
),
|
|
(
|
|
"contract_based_routing_connector_score",
|
|
connector_score.to_string(),
|
|
),
|
|
(
|
|
"current_payment_count_contract_based_routing_connector",
|
|
current_payment_cnt.to_string(),
|
|
),
|
|
("payment_connector", payment_connector.to_string()),
|
|
(
|
|
"currency",
|
|
payment_attempt
|
|
.currency
|
|
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
|
|
),
|
|
(
|
|
"payment_method",
|
|
payment_attempt.payment_method.map_or_else(
|
|
|| "None".to_string(),
|
|
|payment_method| payment_method.to_string(),
|
|
),
|
|
),
|
|
(
|
|
"payment_method_type",
|
|
payment_attempt.payment_method_type.map_or_else(
|
|
|| "None".to_string(),
|
|
|payment_method_type| payment_method_type.to_string(),
|
|
),
|
|
),
|
|
(
|
|
"capture_method",
|
|
payment_attempt.capture_method.map_or_else(
|
|
|| "None".to_string(),
|
|
|capture_method| capture_method.to_string(),
|
|
),
|
|
),
|
|
(
|
|
"authentication_type",
|
|
payment_attempt.authentication_type.map_or_else(
|
|
|| "None".to_string(),
|
|
|authentication_type| authentication_type.to_string(),
|
|
),
|
|
),
|
|
("payment_status", payment_attempt.status.to_string()),
|
|
),
|
|
);
|
|
logger::debug!("successfully pushed contract_based_routing metrics");
|
|
|
|
Ok(())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
fn get_desired_payment_status_for_dynamic_routing_metrics(
|
|
attempt_status: common_enums::AttemptStatus,
|
|
) -> common_enums::AttemptStatus {
|
|
match attempt_status {
|
|
common_enums::AttemptStatus::Charged
|
|
| common_enums::AttemptStatus::Authorized
|
|
| common_enums::AttemptStatus::PartialCharged
|
|
| common_enums::AttemptStatus::PartialChargedAndChargeable => {
|
|
common_enums::AttemptStatus::Charged
|
|
}
|
|
common_enums::AttemptStatus::Failure
|
|
| common_enums::AttemptStatus::AuthorizationFailed
|
|
| common_enums::AttemptStatus::AuthenticationFailed
|
|
| common_enums::AttemptStatus::CaptureFailed
|
|
| common_enums::AttemptStatus::RouterDeclined => common_enums::AttemptStatus::Failure,
|
|
common_enums::AttemptStatus::Started
|
|
| common_enums::AttemptStatus::AuthenticationPending
|
|
| common_enums::AttemptStatus::AuthenticationSuccessful
|
|
| common_enums::AttemptStatus::Authorizing
|
|
| common_enums::AttemptStatus::CodInitiated
|
|
| common_enums::AttemptStatus::Voided
|
|
| common_enums::AttemptStatus::VoidInitiated
|
|
| common_enums::AttemptStatus::CaptureInitiated
|
|
| common_enums::AttemptStatus::VoidFailed
|
|
| common_enums::AttemptStatus::AutoRefunded
|
|
| common_enums::AttemptStatus::Unresolved
|
|
| common_enums::AttemptStatus::Pending
|
|
| common_enums::AttemptStatus::PaymentMethodAwaited
|
|
| common_enums::AttemptStatus::ConfirmationAwaited
|
|
| common_enums::AttemptStatus::DeviceDataCollectionPending => {
|
|
common_enums::AttemptStatus::Pending
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
fn get_dynamic_routing_based_metrics_outcome_for_payment(
|
|
payment_status_attribute: common_enums::AttemptStatus,
|
|
payment_connector: String,
|
|
first_success_based_connector: String,
|
|
) -> common_enums::SuccessBasedRoutingConclusiveState {
|
|
match payment_status_attribute {
|
|
common_enums::AttemptStatus::Charged
|
|
if *first_success_based_connector == *payment_connector =>
|
|
{
|
|
common_enums::SuccessBasedRoutingConclusiveState::TruePositive
|
|
}
|
|
common_enums::AttemptStatus::Failure
|
|
if *first_success_based_connector == *payment_connector =>
|
|
{
|
|
common_enums::SuccessBasedRoutingConclusiveState::FalsePositive
|
|
}
|
|
common_enums::AttemptStatus::Failure
|
|
if *first_success_based_connector != *payment_connector =>
|
|
{
|
|
common_enums::SuccessBasedRoutingConclusiveState::TrueNegative
|
|
}
|
|
common_enums::AttemptStatus::Charged
|
|
if *first_success_based_connector != *payment_connector =>
|
|
{
|
|
common_enums::SuccessBasedRoutingConclusiveState::FalseNegative
|
|
}
|
|
_ => common_enums::SuccessBasedRoutingConclusiveState::NonDeterministic,
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
pub async fn disable_dynamic_routing_algorithm(
|
|
state: &SessionState,
|
|
key_store: domain::MerchantKeyStore,
|
|
business_profile: domain::Profile,
|
|
dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
|
|
dynamic_routing_type: routing_types::DynamicRoutingType,
|
|
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
|
|
let db = state.store.as_ref();
|
|
let key_manager_state = &state.into();
|
|
let profile_id = business_profile.get_id().clone();
|
|
let (algorithm_id, dynamic_routing_algorithm, cache_entries_to_redact) =
|
|
match dynamic_routing_type {
|
|
routing_types::DynamicRoutingType::SuccessRateBasedRouting => {
|
|
let Some(algorithm_ref) = dynamic_routing_algo_ref.success_based_algorithm else {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Success rate based routing is already disabled".to_string(),
|
|
})?
|
|
};
|
|
let Some(algorithm_id) = algorithm_ref.algorithm_id_with_timestamp.algorithm_id
|
|
else {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Algorithm is already inactive".to_string(),
|
|
})?
|
|
};
|
|
|
|
let cache_key = format!(
|
|
"{}_{}",
|
|
business_profile.get_id().get_string_repr(),
|
|
algorithm_id.get_string_repr()
|
|
);
|
|
let cache_entries_to_redact =
|
|
vec![cache::CacheKind::SuccessBasedDynamicRoutingCache(
|
|
cache_key.into(),
|
|
)];
|
|
(
|
|
algorithm_id,
|
|
routing_types::DynamicRoutingAlgorithmRef {
|
|
success_based_algorithm: Some(routing_types::SuccessBasedAlgorithm {
|
|
algorithm_id_with_timestamp:
|
|
routing_types::DynamicAlgorithmWithTimestamp::new(None),
|
|
enabled_feature: routing_types::DynamicRoutingFeatures::None,
|
|
}),
|
|
elimination_routing_algorithm: dynamic_routing_algo_ref
|
|
.elimination_routing_algorithm,
|
|
contract_based_routing: dynamic_routing_algo_ref.contract_based_routing,
|
|
dynamic_routing_volume_split: dynamic_routing_algo_ref
|
|
.dynamic_routing_volume_split,
|
|
},
|
|
cache_entries_to_redact,
|
|
)
|
|
}
|
|
routing_types::DynamicRoutingType::EliminationRouting => {
|
|
let Some(algorithm_ref) = dynamic_routing_algo_ref.elimination_routing_algorithm
|
|
else {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Elimination routing is already disabled".to_string(),
|
|
})?
|
|
};
|
|
let Some(algorithm_id) = algorithm_ref.algorithm_id_with_timestamp.algorithm_id
|
|
else {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Algorithm is already inactive".to_string(),
|
|
})?
|
|
};
|
|
let cache_key = format!(
|
|
"{}_{}",
|
|
business_profile.get_id().get_string_repr(),
|
|
algorithm_id.get_string_repr()
|
|
);
|
|
let cache_entries_to_redact =
|
|
vec![cache::CacheKind::EliminationBasedDynamicRoutingCache(
|
|
cache_key.into(),
|
|
)];
|
|
(
|
|
algorithm_id,
|
|
routing_types::DynamicRoutingAlgorithmRef {
|
|
success_based_algorithm: dynamic_routing_algo_ref.success_based_algorithm,
|
|
dynamic_routing_volume_split: dynamic_routing_algo_ref
|
|
.dynamic_routing_volume_split,
|
|
elimination_routing_algorithm: Some(
|
|
routing_types::EliminationRoutingAlgorithm {
|
|
algorithm_id_with_timestamp:
|
|
routing_types::DynamicAlgorithmWithTimestamp::new(None),
|
|
enabled_feature: routing_types::DynamicRoutingFeatures::None,
|
|
},
|
|
),
|
|
contract_based_routing: dynamic_routing_algo_ref.contract_based_routing,
|
|
},
|
|
cache_entries_to_redact,
|
|
)
|
|
}
|
|
routing_types::DynamicRoutingType::ContractBasedRouting => {
|
|
let Some(algorithm_ref) = dynamic_routing_algo_ref.contract_based_routing else {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Contract routing is already disabled".to_string(),
|
|
})?
|
|
};
|
|
let Some(algorithm_id) = algorithm_ref.algorithm_id_with_timestamp.algorithm_id
|
|
else {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Algorithm is already inactive".to_string(),
|
|
})?
|
|
};
|
|
let cache_key = format!(
|
|
"{}_{}",
|
|
business_profile.get_id().get_string_repr(),
|
|
algorithm_id.get_string_repr()
|
|
);
|
|
let cache_entries_to_redact =
|
|
vec![cache::CacheKind::ContractBasedDynamicRoutingCache(
|
|
cache_key.into(),
|
|
)];
|
|
(
|
|
algorithm_id,
|
|
routing_types::DynamicRoutingAlgorithmRef {
|
|
success_based_algorithm: dynamic_routing_algo_ref.success_based_algorithm,
|
|
elimination_routing_algorithm: dynamic_routing_algo_ref
|
|
.elimination_routing_algorithm,
|
|
dynamic_routing_volume_split: dynamic_routing_algo_ref
|
|
.dynamic_routing_volume_split,
|
|
contract_based_routing: Some(routing_types::ContractRoutingAlgorithm {
|
|
algorithm_id_with_timestamp:
|
|
routing_types::DynamicAlgorithmWithTimestamp::new(None),
|
|
enabled_feature: routing_types::DynamicRoutingFeatures::None,
|
|
}),
|
|
},
|
|
cache_entries_to_redact,
|
|
)
|
|
}
|
|
};
|
|
|
|
// redact cache for dynamic routing config
|
|
let _ = cache::redact_from_redis_and_publish(
|
|
state.store.get_cache_store().as_ref(),
|
|
cache_entries_to_redact,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"unable to publish into the redact channel for evicting the dynamic routing config cache",
|
|
)?;
|
|
|
|
let record = db
|
|
.find_routing_algorithm_by_profile_id_algorithm_id(business_profile.get_id(), &algorithm_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
|
|
let response = record.foreign_into();
|
|
update_business_profile_active_dynamic_algorithm_ref(
|
|
db,
|
|
key_manager_state,
|
|
&key_store,
|
|
business_profile,
|
|
dynamic_routing_algorithm,
|
|
)
|
|
.await?;
|
|
|
|
core_metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(
|
|
1,
|
|
router_env::metric_attributes!(("profile_id", profile_id)),
|
|
);
|
|
|
|
Ok(ApplicationResponse::Json(response))
|
|
}
|
|
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
pub async fn enable_dynamic_routing_algorithm(
|
|
state: &SessionState,
|
|
key_store: domain::MerchantKeyStore,
|
|
business_profile: domain::Profile,
|
|
feature_to_enable: routing_types::DynamicRoutingFeatures,
|
|
dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
|
|
dynamic_routing_type: routing_types::DynamicRoutingType,
|
|
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
|
|
let mut dynamic_routing = dynamic_routing_algo_ref.clone();
|
|
match dynamic_routing_type {
|
|
routing_types::DynamicRoutingType::SuccessRateBasedRouting => {
|
|
dynamic_routing
|
|
.disable_algorithm_id(routing_types::DynamicRoutingType::ContractBasedRouting);
|
|
|
|
enable_specific_routing_algorithm(
|
|
state,
|
|
key_store,
|
|
business_profile,
|
|
feature_to_enable,
|
|
dynamic_routing.clone(),
|
|
dynamic_routing_type,
|
|
dynamic_routing.success_based_algorithm,
|
|
)
|
|
.await
|
|
}
|
|
routing_types::DynamicRoutingType::EliminationRouting => {
|
|
enable_specific_routing_algorithm(
|
|
state,
|
|
key_store,
|
|
business_profile,
|
|
feature_to_enable,
|
|
dynamic_routing.clone(),
|
|
dynamic_routing_type,
|
|
dynamic_routing.elimination_routing_algorithm,
|
|
)
|
|
.await
|
|
}
|
|
routing_types::DynamicRoutingType::ContractBasedRouting => {
|
|
Err((errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "Contract routing cannot be set as default".to_string(),
|
|
})
|
|
.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
pub async fn enable_specific_routing_algorithm<A>(
|
|
state: &SessionState,
|
|
key_store: domain::MerchantKeyStore,
|
|
business_profile: domain::Profile,
|
|
feature_to_enable: routing_types::DynamicRoutingFeatures,
|
|
mut dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
|
|
dynamic_routing_type: routing_types::DynamicRoutingType,
|
|
algo_type: Option<A>,
|
|
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>>
|
|
where
|
|
A: routing_types::DynamicRoutingAlgoAccessor + Clone + Debug,
|
|
{
|
|
// Algorithm wasn't created yet
|
|
let Some(mut algo_type) = algo_type else {
|
|
return default_specific_dynamic_routing_setup(
|
|
state,
|
|
key_store,
|
|
business_profile,
|
|
feature_to_enable,
|
|
dynamic_routing_algo_ref,
|
|
dynamic_routing_type,
|
|
)
|
|
.await;
|
|
};
|
|
|
|
// Algorithm was in disabled state
|
|
let Some(algo_type_algorithm_id) = algo_type
|
|
.clone()
|
|
.get_algorithm_id_with_timestamp()
|
|
.algorithm_id
|
|
else {
|
|
return default_specific_dynamic_routing_setup(
|
|
state,
|
|
key_store,
|
|
business_profile,
|
|
feature_to_enable,
|
|
dynamic_routing_algo_ref,
|
|
dynamic_routing_type,
|
|
)
|
|
.await;
|
|
};
|
|
let db = state.store.as_ref();
|
|
let profile_id = business_profile.get_id().clone();
|
|
let algo_type_enabled_features = algo_type.get_enabled_features();
|
|
if *algo_type_enabled_features == feature_to_enable {
|
|
// algorithm already has the required feature
|
|
return Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: format!("{} is already enabled", dynamic_routing_type),
|
|
}
|
|
.into());
|
|
};
|
|
*algo_type_enabled_features = feature_to_enable;
|
|
dynamic_routing_algo_ref.update_enabled_features(dynamic_routing_type, feature_to_enable);
|
|
update_business_profile_active_dynamic_algorithm_ref(
|
|
db,
|
|
&state.into(),
|
|
&key_store,
|
|
business_profile,
|
|
dynamic_routing_algo_ref.clone(),
|
|
)
|
|
.await?;
|
|
|
|
let routing_algorithm = db
|
|
.find_routing_algorithm_by_profile_id_algorithm_id(&profile_id, &algo_type_algorithm_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
|
|
let updated_routing_record = routing_algorithm.foreign_into();
|
|
core_metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
|
1,
|
|
router_env::metric_attributes!(("profile_id", profile_id.clone())),
|
|
);
|
|
Ok(ApplicationResponse::Json(updated_routing_record))
|
|
}
|
|
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
#[instrument(skip_all)]
|
|
pub async fn default_specific_dynamic_routing_setup(
|
|
state: &SessionState,
|
|
key_store: domain::MerchantKeyStore,
|
|
business_profile: domain::Profile,
|
|
feature_to_enable: routing_types::DynamicRoutingFeatures,
|
|
mut dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef,
|
|
dynamic_routing_type: routing_types::DynamicRoutingType,
|
|
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
|
|
let db = state.store.as_ref();
|
|
let key_manager_state = &state.into();
|
|
let profile_id = business_profile.get_id().clone();
|
|
let merchant_id = business_profile.merchant_id.clone();
|
|
let algorithm_id = common_utils::generate_routing_id_of_default_length();
|
|
let timestamp = common_utils::date_time::now();
|
|
let algo = match dynamic_routing_type {
|
|
routing_types::DynamicRoutingType::SuccessRateBasedRouting => {
|
|
let default_success_based_routing_config =
|
|
routing_types::SuccessBasedRoutingConfig::default();
|
|
routing_algorithm::RoutingAlgorithm {
|
|
algorithm_id: algorithm_id.clone(),
|
|
profile_id: profile_id.clone(),
|
|
merchant_id,
|
|
name: SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM.to_string(),
|
|
description: None,
|
|
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
|
|
algorithm_data: serde_json::json!(default_success_based_routing_config),
|
|
created_at: timestamp,
|
|
modified_at: timestamp,
|
|
algorithm_for: common_enums::TransactionType::Payment,
|
|
}
|
|
}
|
|
routing_types::DynamicRoutingType::EliminationRouting => {
|
|
let default_elimination_routing_config =
|
|
routing_types::EliminationRoutingConfig::default();
|
|
routing_algorithm::RoutingAlgorithm {
|
|
algorithm_id: algorithm_id.clone(),
|
|
profile_id: profile_id.clone(),
|
|
merchant_id,
|
|
name: ELIMINATION_BASED_DYNAMIC_ROUTING_ALGORITHM.to_string(),
|
|
description: None,
|
|
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
|
|
algorithm_data: serde_json::json!(default_elimination_routing_config),
|
|
created_at: timestamp,
|
|
modified_at: timestamp,
|
|
algorithm_for: common_enums::TransactionType::Payment,
|
|
}
|
|
}
|
|
|
|
routing_types::DynamicRoutingType::ContractBasedRouting => {
|
|
return Err((errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "Contract routing cannot be set as default".to_string(),
|
|
})
|
|
.into())
|
|
}
|
|
};
|
|
|
|
let record = db
|
|
.insert_routing_algorithm(algo)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to insert record in routing algorithm table")?;
|
|
|
|
dynamic_routing_algo_ref.update_algorithm_id(
|
|
algorithm_id,
|
|
feature_to_enable,
|
|
dynamic_routing_type,
|
|
);
|
|
update_business_profile_active_dynamic_algorithm_ref(
|
|
db,
|
|
key_manager_state,
|
|
&key_store,
|
|
business_profile,
|
|
dynamic_routing_algo_ref,
|
|
)
|
|
.await?;
|
|
|
|
let new_record = record.foreign_into();
|
|
|
|
core_metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
|
1,
|
|
router_env::metric_attributes!(("profile_id", profile_id.clone())),
|
|
);
|
|
Ok(ApplicationResponse::Json(new_record))
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DynamicRoutingConfigParamsInterpolator {
|
|
pub payment_method: Option<common_enums::PaymentMethod>,
|
|
pub payment_method_type: Option<common_enums::PaymentMethodType>,
|
|
pub authentication_type: Option<common_enums::AuthenticationType>,
|
|
pub currency: Option<common_enums::Currency>,
|
|
pub country: Option<common_enums::CountryAlpha2>,
|
|
pub card_network: Option<String>,
|
|
pub card_bin: Option<String>,
|
|
}
|
|
|
|
impl DynamicRoutingConfigParamsInterpolator {
|
|
pub fn new(
|
|
payment_method: Option<common_enums::PaymentMethod>,
|
|
payment_method_type: Option<common_enums::PaymentMethodType>,
|
|
authentication_type: Option<common_enums::AuthenticationType>,
|
|
currency: Option<common_enums::Currency>,
|
|
country: Option<common_enums::CountryAlpha2>,
|
|
card_network: Option<String>,
|
|
card_bin: Option<String>,
|
|
) -> Self {
|
|
Self {
|
|
payment_method,
|
|
payment_method_type,
|
|
authentication_type,
|
|
currency,
|
|
country,
|
|
card_network,
|
|
card_bin,
|
|
}
|
|
}
|
|
|
|
pub fn get_string_val(
|
|
&self,
|
|
params: &Vec<routing_types::DynamicRoutingConfigParams>,
|
|
) -> String {
|
|
let mut parts: Vec<String> = Vec::new();
|
|
for param in params {
|
|
let val = match param {
|
|
routing_types::DynamicRoutingConfigParams::PaymentMethod => self
|
|
.payment_method
|
|
.as_ref()
|
|
.map_or(String::new(), |pm| pm.to_string()),
|
|
routing_types::DynamicRoutingConfigParams::PaymentMethodType => self
|
|
.payment_method_type
|
|
.as_ref()
|
|
.map_or(String::new(), |pmt| pmt.to_string()),
|
|
routing_types::DynamicRoutingConfigParams::AuthenticationType => self
|
|
.authentication_type
|
|
.as_ref()
|
|
.map_or(String::new(), |at| at.to_string()),
|
|
routing_types::DynamicRoutingConfigParams::Currency => self
|
|
.currency
|
|
.as_ref()
|
|
.map_or(String::new(), |cur| cur.to_string()),
|
|
routing_types::DynamicRoutingConfigParams::Country => self
|
|
.country
|
|
.as_ref()
|
|
.map_or(String::new(), |cn| cn.to_string()),
|
|
routing_types::DynamicRoutingConfigParams::CardNetwork => {
|
|
self.card_network.clone().unwrap_or_default()
|
|
}
|
|
routing_types::DynamicRoutingConfigParams::CardBin => {
|
|
self.card_bin.clone().unwrap_or_default()
|
|
}
|
|
};
|
|
if !val.is_empty() {
|
|
parts.push(val);
|
|
}
|
|
}
|
|
parts.join(":")
|
|
}
|
|
}
|