mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
refactor(debit_routing): filter debit networks based on merchant connector account configuration (#8175)
This commit is contained in:
@ -2,7 +2,9 @@ use std::{collections::HashSet, fmt::Debug};
|
||||
|
||||
use api_models::{enums as api_enums, open_router};
|
||||
use common_enums::enums;
|
||||
use common_utils::id_type;
|
||||
use common_utils::{
|
||||
errors::CustomResult, ext_traits::ValueExt, id_type, types::keymanager::KeyManagerState,
|
||||
};
|
||||
use error_stack::ResultExt;
|
||||
use masking::Secret;
|
||||
|
||||
@ -22,6 +24,7 @@ use crate::{
|
||||
api::{self, ConnectorCallType},
|
||||
domain,
|
||||
},
|
||||
utils::id_type::MerchantConnectorAccountId,
|
||||
};
|
||||
|
||||
pub struct DebitRoutingResult {
|
||||
@ -65,7 +68,6 @@ where
|
||||
logger::info!("Performing debit routing for PreDetermined connector");
|
||||
handle_pre_determined_connector(
|
||||
state,
|
||||
&debit_routing_config,
|
||||
debit_routing_supported_connectors,
|
||||
&connector_data,
|
||||
payment_data,
|
||||
@ -77,7 +79,6 @@ where
|
||||
logger::info!("Performing debit routing for Retryable connector");
|
||||
handle_retryable_connector(
|
||||
state,
|
||||
&debit_routing_config,
|
||||
debit_routing_supported_connectors,
|
||||
connector_data,
|
||||
payment_data,
|
||||
@ -234,7 +235,6 @@ pub async fn check_for_debit_routing_connector_in_profile<
|
||||
|
||||
async fn handle_pre_determined_connector<F, D>(
|
||||
state: &SessionState,
|
||||
debit_routing_config: &settings::DebitRoutingConfig,
|
||||
debit_routing_supported_connectors: HashSet<api_enums::Connector>,
|
||||
connector_data: &api::ConnectorRoutingData,
|
||||
payment_data: &mut D,
|
||||
@ -244,6 +244,11 @@ where
|
||||
F: Send + Clone,
|
||||
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
||||
{
|
||||
let db = state.store.as_ref();
|
||||
let key_manager_state = &(state).into();
|
||||
let merchant_id = payment_data.get_payment_attempt().merchant_id.clone();
|
||||
let profile_id = payment_data.get_payment_attempt().profile_id.clone();
|
||||
|
||||
if debit_routing_supported_connectors.contains(&connector_data.connector_data.connector_name) {
|
||||
logger::debug!("Chosen connector is supported for debit routing");
|
||||
|
||||
@ -255,15 +260,43 @@ where
|
||||
debit_routing_output.co_badged_card_networks
|
||||
);
|
||||
|
||||
let valid_connectors = build_connector_routing_data(
|
||||
connector_data,
|
||||
debit_routing_config,
|
||||
&debit_routing_output.co_badged_card_networks,
|
||||
);
|
||||
let key_store = db
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
key_manager_state,
|
||||
&merchant_id,
|
||||
&db.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::MerchantAccountNotFound)
|
||||
.map_err(|error| {
|
||||
logger::error!(
|
||||
"Failed to get merchant key store by merchant_id {:?}",
|
||||
error
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if !valid_connectors.is_empty() {
|
||||
let connector_routing_data = build_connector_routing_data(
|
||||
state,
|
||||
&profile_id,
|
||||
&key_store,
|
||||
vec![connector_data.clone()],
|
||||
debit_routing_output.co_badged_card_networks.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
logger::error!(
|
||||
"Failed to build connector routing data for debit routing {:?}",
|
||||
error
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if !connector_routing_data.is_empty() {
|
||||
return Some(DebitRoutingResult {
|
||||
debit_routing_connector_call_type: ConnectorCallType::Retryable(valid_connectors),
|
||||
debit_routing_connector_call_type: ConnectorCallType::Retryable(
|
||||
connector_routing_data,
|
||||
),
|
||||
debit_routing_output,
|
||||
});
|
||||
}
|
||||
@ -377,44 +410,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn check_connector_support_for_network(
|
||||
debit_routing_config: &settings::DebitRoutingConfig,
|
||||
connector_name: api_enums::Connector,
|
||||
network: &enums::CardNetwork,
|
||||
) -> Option<enums::CardNetwork> {
|
||||
debit_routing_config
|
||||
.connector_supported_debit_networks
|
||||
.get(&connector_name)
|
||||
.and_then(|supported_networks| {
|
||||
(supported_networks.contains(network) || network.is_global_network())
|
||||
.then(|| network.clone())
|
||||
})
|
||||
}
|
||||
|
||||
fn build_connector_routing_data(
|
||||
connector_data: &api::ConnectorRoutingData,
|
||||
debit_routing_config: &settings::DebitRoutingConfig,
|
||||
fee_sorted_debit_networks: &[enums::CardNetwork],
|
||||
) -> Vec<api::ConnectorRoutingData> {
|
||||
fee_sorted_debit_networks
|
||||
.iter()
|
||||
.filter_map(|network| {
|
||||
check_connector_support_for_network(
|
||||
debit_routing_config,
|
||||
connector_data.connector_data.connector_name,
|
||||
network,
|
||||
)
|
||||
.map(|valid_network| api::ConnectorRoutingData {
|
||||
connector_data: connector_data.connector_data.clone(),
|
||||
network: Some(valid_network),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn handle_retryable_connector<F, D>(
|
||||
state: &SessionState,
|
||||
debit_routing_config: &settings::DebitRoutingConfig,
|
||||
debit_routing_supported_connectors: HashSet<api_enums::Connector>,
|
||||
connector_data_list: Vec<api::ConnectorRoutingData>,
|
||||
payment_data: &mut D,
|
||||
@ -424,6 +421,10 @@ where
|
||||
F: Send + Clone,
|
||||
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
||||
{
|
||||
let key_manager_state = &(state).into();
|
||||
let db = state.store.as_ref();
|
||||
let profile_id = payment_data.get_payment_attempt().profile_id.clone();
|
||||
let merchant_id = payment_data.get_payment_attempt().merchant_id.clone();
|
||||
let is_any_debit_routing_connector_supported =
|
||||
connector_data_list.iter().any(|connector_data| {
|
||||
debit_routing_supported_connectors
|
||||
@ -433,32 +434,212 @@ where
|
||||
if is_any_debit_routing_connector_supported {
|
||||
let debit_routing_output =
|
||||
get_debit_routing_output::<F, D>(state, payment_data, acquirer_country).await?;
|
||||
|
||||
logger::debug!(
|
||||
"Sorted co-badged networks: {:?}",
|
||||
debit_routing_output.co_badged_card_networks
|
||||
);
|
||||
|
||||
let supported_connectors: Vec<_> = connector_data_list
|
||||
.iter()
|
||||
.flat_map(|connector_data| {
|
||||
build_connector_routing_data(
|
||||
connector_data,
|
||||
debit_routing_config,
|
||||
&debit_routing_output.co_badged_card_networks,
|
||||
let key_store = db
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
key_manager_state,
|
||||
&merchant_id,
|
||||
&db.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::MerchantAccountNotFound)
|
||||
.map_err(|error| {
|
||||
logger::error!(
|
||||
"Failed to get merchant key store by merchant_id {:?}",
|
||||
error
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
.ok()?;
|
||||
|
||||
if !supported_connectors.is_empty() {
|
||||
let connector_routing_data = build_connector_routing_data(
|
||||
state,
|
||||
&profile_id,
|
||||
&key_store,
|
||||
connector_data_list.clone(),
|
||||
debit_routing_output.co_badged_card_networks.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
logger::error!(
|
||||
"Failed to build connector routing data for debit routing {:?}",
|
||||
error
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if !connector_routing_data.is_empty() {
|
||||
return Some(DebitRoutingResult {
|
||||
debit_routing_connector_call_type: ConnectorCallType::Retryable(
|
||||
supported_connectors,
|
||||
connector_routing_data,
|
||||
),
|
||||
debit_routing_output,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn build_connector_routing_data(
|
||||
state: &SessionState,
|
||||
profile_id: &id_type::ProfileId,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
eligible_connector_data_list: Vec<api::ConnectorRoutingData>,
|
||||
fee_sorted_debit_networks: Vec<common_enums::CardNetwork>,
|
||||
) -> CustomResult<Vec<api::ConnectorRoutingData>, errors::ApiErrorResponse> {
|
||||
let key_manager_state = &state.into();
|
||||
let debit_routing_config = &state.conf.debit_routing_config;
|
||||
|
||||
let mcas_for_profile =
|
||||
fetch_merchant_connector_accounts(state, key_manager_state, profile_id, key_store).await?;
|
||||
|
||||
let mut connector_routing_data = Vec::new();
|
||||
let mut has_us_local_network = false;
|
||||
|
||||
for connector_data in eligible_connector_data_list {
|
||||
if let Some(routing_data) = process_connector_for_networks(
|
||||
&connector_data,
|
||||
&mcas_for_profile,
|
||||
&fee_sorted_debit_networks,
|
||||
debit_routing_config,
|
||||
&mut has_us_local_network,
|
||||
)? {
|
||||
connector_routing_data.extend(routing_data);
|
||||
}
|
||||
}
|
||||
|
||||
validate_us_local_network_requirement(has_us_local_network)?;
|
||||
Ok(connector_routing_data)
|
||||
}
|
||||
|
||||
/// Fetches merchant connector accounts for the given profile
|
||||
async fn fetch_merchant_connector_accounts(
|
||||
state: &SessionState,
|
||||
key_manager_state: &KeyManagerState,
|
||||
profile_id: &id_type::ProfileId,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
) -> CustomResult<Vec<domain::MerchantConnectorAccount>, errors::ApiErrorResponse> {
|
||||
state
|
||||
.store
|
||||
.list_enabled_connector_accounts_by_profile_id(
|
||||
key_manager_state,
|
||||
profile_id,
|
||||
key_store,
|
||||
common_enums::ConnectorType::PaymentProcessor,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to fetch merchant connector accounts")
|
||||
}
|
||||
|
||||
/// Processes a single connector to find matching networks
|
||||
fn process_connector_for_networks(
|
||||
connector_data: &api::ConnectorRoutingData,
|
||||
mcas_for_profile: &[domain::MerchantConnectorAccount],
|
||||
fee_sorted_debit_networks: &[common_enums::CardNetwork],
|
||||
debit_routing_config: &settings::DebitRoutingConfig,
|
||||
has_us_local_network: &mut bool,
|
||||
) -> CustomResult<Option<Vec<api::ConnectorRoutingData>>, errors::ApiErrorResponse> {
|
||||
let Some(merchant_connector_id) = &connector_data.connector_data.merchant_connector_id else {
|
||||
logger::warn!("Skipping connector with missing merchant_connector_id");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(account) = find_merchant_connector_account(mcas_for_profile, merchant_connector_id)
|
||||
else {
|
||||
logger::warn!(
|
||||
"No MCA found for merchant_connector_id: {:?}",
|
||||
merchant_connector_id
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let merchant_debit_networks = extract_debit_networks(&account)?;
|
||||
let matching_networks = find_matching_networks(
|
||||
&merchant_debit_networks,
|
||||
fee_sorted_debit_networks,
|
||||
&connector_data.connector_data,
|
||||
debit_routing_config,
|
||||
has_us_local_network,
|
||||
);
|
||||
|
||||
Ok(Some(matching_networks))
|
||||
}
|
||||
|
||||
/// Finds a merchant connector account by ID
|
||||
fn find_merchant_connector_account(
|
||||
mcas: &[domain::MerchantConnectorAccount],
|
||||
merchant_connector_id: &MerchantConnectorAccountId,
|
||||
) -> Option<domain::MerchantConnectorAccount> {
|
||||
mcas.iter()
|
||||
.find(|mca| mca.merchant_connector_id == *merchant_connector_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Finds networks that match between merchant and fee-sorted networks
|
||||
fn find_matching_networks(
|
||||
merchant_debit_networks: &HashSet<common_enums::CardNetwork>,
|
||||
fee_sorted_debit_networks: &[common_enums::CardNetwork],
|
||||
connector_data: &api::ConnectorData,
|
||||
debit_routing_config: &settings::DebitRoutingConfig,
|
||||
has_us_local_network: &mut bool,
|
||||
) -> Vec<api::ConnectorRoutingData> {
|
||||
let is_routing_enabled = debit_routing_config
|
||||
.supported_connectors
|
||||
.contains(&connector_data.connector_name);
|
||||
|
||||
fee_sorted_debit_networks
|
||||
.iter()
|
||||
.filter(|network| merchant_debit_networks.contains(network))
|
||||
.filter(|network| is_routing_enabled || network.is_global_network())
|
||||
.map(|network| {
|
||||
if network.is_us_local_network() {
|
||||
*has_us_local_network = true;
|
||||
}
|
||||
|
||||
api::ConnectorRoutingData {
|
||||
connector_data: connector_data.clone(),
|
||||
network: Some(network.clone()),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Validates that at least one US local network is present
|
||||
fn validate_us_local_network_requirement(
|
||||
has_us_local_network: bool,
|
||||
) -> CustomResult<(), errors::ApiErrorResponse> {
|
||||
if !has_us_local_network {
|
||||
return Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("At least one US local network is required in routing");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_debit_networks(
|
||||
account: &domain::MerchantConnectorAccount,
|
||||
) -> CustomResult<HashSet<common_enums::CardNetwork>, errors::ApiErrorResponse> {
|
||||
let mut networks = HashSet::new();
|
||||
|
||||
if let Some(values) = &account.payment_methods_enabled {
|
||||
for val in values {
|
||||
let payment_methods_enabled: api_models::admin::PaymentMethodsEnabled =
|
||||
val.to_owned().parse_value("PaymentMethodsEnabled")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to parse enabled payment methods for a merchant connector account in debit routing flow")?;
|
||||
|
||||
if let Some(types) = payment_methods_enabled.payment_method_types {
|
||||
for method_type in types {
|
||||
if method_type.payment_method_type
|
||||
== api_models::enums::PaymentMethodType::Debit
|
||||
{
|
||||
if let Some(card_networks) = method_type.card_networks {
|
||||
networks.extend(card_networks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(networks)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user