//! 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 api_models::routing as routing_types; use common_utils::{ext_traits::Encode, types::keymanager::KeyManagerState}; use diesel_models::configs; use error_stack::ResultExt; use rustc_hash::FxHashSet; use storage_impl::redis::cache; #[cfg(all(feature = "v2", feature = "routing_v2"))] use crate::types::domain::MerchantConnectorAccount; use crate::{ core::errors::{self, RouterResult}, db::StorageInterface, routes::SessionState, types::{domain, storage}, utils::StringExt, }; /// 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> { 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") .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::::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, 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(all( any(feature = "v1", feature = "v2"), not(feature = "merchant_account_v2") ))] 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::publish_into_redact_channel(db.get_cache_store().as_ref(), [config_key]) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to invalidate the config cache")?; Ok(()) } #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] pub async fn update_merchant_active_algorithm_ref( _state: &SessionState, _key_store: &domain::MerchantKeyStore, _config_key: cache::CacheKind<'_>, _algorithm_id: routing_types::RoutingAlgorithmRef, ) -> RouterResult<()> { // TODO: handle updating the active routing algorithm for v2 in merchant account todo!() } #[cfg(all( any(feature = "v1", feature = "v2"), not(any(feature = "routing_v2", feature = "business_profile_v2")) ))] pub async fn update_business_profile_active_algorithm_ref( db: &dyn StorageInterface, key_manager_state: &KeyManagerState, merchant_key_store: &domain::MerchantKeyStore, current_business_profile: domain::BusinessProfile, 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.profile_id.clone(); 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::BusinessProfileUpdate::RoutingAlgorithmUpdate { routing_algorithm, payout_routing_algorithm, }; db.update_business_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::publish_into_redact_channel(db.get_cache_store().as_ref(), [routing_cache_key]) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to invalidate routing cache")?; Ok(()) } #[cfg(all(feature = "v2", feature = "routing_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 String, common_utils::id_type::MerchantConnectorAccountId, )>, ); #[derive(Clone, Debug)] pub struct ConnectNameForProfile<'a>(pub FxHashSet<&'a String>); #[cfg(all(feature = "v2", feature = "routing_v2"))] #[derive(Clone, Debug)] pub struct MerchantConnectorAccounts(pub Vec); #[cfg(all(feature = "v2", feature = "routing_v2"))] impl MerchantConnectorAccounts { pub async fn get_all_mcas( merchant_id: &common_utils::id_type::MerchantId, key_store: &domain::MerchantKeyStore, state: &SessionState, ) -> RouterResult { 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 where T: std::hash::Hash + Eq, { self.0 .iter() .filter(|mca| filter(mca)) .map(func) .collect::>() } pub fn filter_by_profile<'a, T>( &'a self, profile_id: &'a common_utils::id_type::ProfileId, func: impl Fn(&'a MerchantConnectorAccount) -> T, ) -> FxHashSet where T: std::hash::Hash + Eq, { self.filter_and_map(|mca| mca.profile_id == *profile_id, func) } } #[cfg(all(feature = "v2", feature = "routing_v2"))] impl<'h> RoutingAlgorithmHelpers<'h> { fn connector_choice( &self, choice: &routing_types::RoutableConnectorChoice, ) -> RouterResult<()> { if let Some(ref mca_id) = choice.merchant_connector_id { error_stack::ensure!( self.name_mca_id_set.0.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!( self.name_set.0.contains(&choice.connector.to_string()), errors::ApiErrorResponse::InvalidRequestData { message: format!( "connector with name '{}' not found for the given profile", choice.connector, ) } ); }; 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(all(any(feature = "v1", feature = "v2"), not(feature = "routing_v2")))] pub async fn validate_connectors_in_routing_config( state: &SessionState, key_store: &domain::MerchantKeyStore, merchant_id: &common_utils::id_type::MerchantId, profile_id: &common_utils::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::>(); let name_set = all_mcas .iter() .filter(|mca| mca.profile_id == *profile_id) .map(|mca| &mca.connector_name) .collect::>(); 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}"), } }