mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	 771f48cfe0
			
		
	
	771f48cfe0
	
	
	
		
			
			Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sanchith Hegde <sanchith.hegde@juspay.in>
		
			
				
	
	
		
			525 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			525 lines
		
	
	
		
			18 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 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<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(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<MerchantConnectorAccount>);
 | |
| 
 | |
| #[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<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 common_utils::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(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::<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}"),
 | |
|     }
 | |
| }
 |