Files
Sai Harsha Vardhan 771f48cfe0 refactor(router): add domain type for merchant_connector_account id (#5685)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Sanchith Hegde <sanchith.hegde@juspay.in>
2024-08-28 07:52:19 +00:00

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}"),
}
}