refactor(debit_routing): filter debit networks based on merchant connector account configuration (#8175)

This commit is contained in:
Shankar Singh C
2025-06-12 18:12:37 +05:30
committed by GitHub
parent d33e344f82
commit 5f97b7bce5

View File

@ -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)
}