refactor(config): change UCS connector list from array to comma-separated string (#8905)

This commit is contained in:
Uzair Khan
2025-08-13 20:24:27 +05:30
committed by GitHub
parent 6950c04eae
commit 9f055e10a2
10 changed files with 206 additions and 12 deletions

View File

@ -1159,6 +1159,7 @@ url = "http://localhost:8080" # Open Router URL
[grpc_client.unified_connector_service] [grpc_client.unified_connector_service]
base_url = "http://localhost:8000" # Unified Connector Service Base URL base_url = "http://localhost:8000" # Unified Connector Service Base URL
connection_timeout = 10 # Connection Timeout Duration in Seconds connection_timeout = 10 # Connection Timeout Duration in Seconds
ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only
[grpc_client.recovery_decider_client] # Revenue recovery client base url [grpc_client.recovery_decider_client] # Revenue recovery client base url
base_url = "http://127.0.0.1:8080" #Base URL base_url = "http://127.0.0.1:8080" #Base URL

View File

@ -384,6 +384,7 @@ connector_names = "connector_names" # Comma-separated list of allowed connec
[grpc_client.unified_connector_service] [grpc_client.unified_connector_service]
base_url = "http://localhost:8000" # Unified Connector Service Base URL base_url = "http://localhost:8000" # Unified Connector Service Base URL
connection_timeout = 10 # Connection Timeout Duration in Seconds connection_timeout = 10 # Connection Timeout Duration in Seconds
ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only
[revenue_recovery.recovery_timestamp] # Timestamp configuration for Revenue Recovery [revenue_recovery.recovery_timestamp] # Timestamp configuration for Revenue Recovery
initial_timestamp_in_hours = 1 # number of hours added to start time for Decider service of Revenue Recovery initial_timestamp_in_hours = 1 # number of hours added to start time for Decider service of Revenue Recovery

View File

@ -837,3 +837,6 @@ click_to_pay = {connector_list = "adyen, cybersource, trustpay"}
[list_dispute_supported_connectors] [list_dispute_supported_connectors]
connector_list = "worldpayvantiv" connector_list = "worldpayvantiv"
[grpc_client.unified_connector_service]
ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only

View File

@ -847,3 +847,6 @@ click_to_pay = {connector_list = "adyen, cybersource, trustpay"}
[revenue_recovery] [revenue_recovery]
monitoring_threshold_in_seconds = 60 monitoring_threshold_in_seconds = 60
retry_algorithm_type = "cascading" retry_algorithm_type = "cascading"
[grpc_client.unified_connector_service]
ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only

View File

@ -856,3 +856,6 @@ retry_algorithm_type = "cascading"
[list_dispute_supported_connectors] [list_dispute_supported_connectors]
connector_list = "worldpayvantiv" connector_list = "worldpayvantiv"
[grpc_client.unified_connector_service]
ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only

View File

@ -1268,12 +1268,7 @@ enabled = "true"
[grpc_client.unified_connector_service] [grpc_client.unified_connector_service]
base_url = "http://localhost:8000" base_url = "http://localhost:8000"
connection_timeout = 10 connection_timeout = 10
ucs_only_connectors = [ ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only
"razorpay",
"phonepe",
"paytm",
"cashfree",
]
[revenue_recovery] [revenue_recovery]
monitoring_threshold_in_seconds = 60 monitoring_threshold_in_seconds = 60

View File

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use common_enums::connector_enums::Connector;
use common_utils::{consts as common_utils_consts, errors::CustomResult, types::Url}; use common_utils::{consts as common_utils_consts, errors::CustomResult, types::Url};
use error_stack::ResultExt; use error_stack::ResultExt;
use masking::{PeekInterface, Secret}; use masking::{PeekInterface, Secret};
@ -18,6 +19,7 @@ use unified_connector_service_client::payments::{
use crate::{ use crate::{
consts, consts,
grpc_client::{GrpcClientSettings, GrpcHeaders}, grpc_client::{GrpcClientSettings, GrpcHeaders},
utils::deserialize_hashset,
}; };
/// Unified Connector Service error variants /// Unified Connector Service error variants
@ -123,9 +125,9 @@ pub struct UnifiedConnectorServiceClientConfig {
/// Contains the connection timeout duration in seconds /// Contains the connection timeout duration in seconds
pub connection_timeout: u64, pub connection_timeout: u64,
/// List of connectors to use with the unified connector service /// Set of external services/connectors available for the unified connector service
#[serde(default)] #[serde(default, deserialize_with = "deserialize_hashset")]
pub ucs_only_connectors: Vec<String>, pub ucs_only_connectors: HashSet<Connector>,
} }
/// Contains the Connector Auth Type and related authentication data. /// Contains the Connector Auth Type and related authentication data.

View File

@ -28,6 +28,9 @@ pub mod managers;
/// crm module /// crm module
pub mod crm; pub mod crm;
/// deserializers module_path
pub mod utils;
#[cfg(feature = "revenue_recovery")] #[cfg(feature = "revenue_recovery")]
/// date_time module /// date_time module
pub mod date_time { pub mod date_time {

View File

@ -0,0 +1,178 @@
//! Custom deserializers for external services configuration
use std::collections::HashSet;
use serde::Deserialize;
/// Parses a comma-separated string into a HashSet of typed values.
///
/// # Arguments
///
/// * `value` - String or string reference containing comma-separated values
///
/// # Returns
///
/// * `Ok(HashSet<T>)` - Successfully parsed HashSet
/// * `Err(String)` - Error message if any value parsing fails
///
/// # Type Parameters
///
/// * `T` - Target type that implements `FromStr`, `Eq`, and `Hash`
///
/// # Examples
///
/// ```
/// use std::collections::HashSet;
///
/// let result: Result<HashSet<i32>, String> =
/// deserialize_hashset_inner("1,2,3");
/// assert!(result.is_ok());
///
/// if let Ok(hashset) = result {
/// assert!(hashset.contains(&1));
/// assert!(hashset.contains(&2));
/// assert!(hashset.contains(&3));
/// }
/// ```
fn deserialize_hashset_inner<T>(value: impl AsRef<str>) -> Result<HashSet<T>, String>
where
T: Eq + std::str::FromStr + std::hash::Hash,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
let (values, errors) = value
.as_ref()
.trim()
.split(',')
.map(|s| {
T::from_str(s.trim()).map_err(|error| {
format!(
"Unable to deserialize `{}` as `{}`: {error}",
s.trim(),
std::any::type_name::<T>()
)
})
})
.fold(
(HashSet::new(), Vec::new()),
|(mut values, mut errors), result| match result {
Ok(t) => {
values.insert(t);
(values, errors)
}
Err(error) => {
errors.push(error);
(values, errors)
}
},
);
if !errors.is_empty() {
Err(format!("Some errors occurred:\n{}", errors.join("\n")))
} else {
Ok(values)
}
}
/// Serde deserializer function for converting comma-separated strings into typed HashSets.
///
/// This function is designed to be used with serde's `#[serde(deserialize_with = "deserialize_hashset")]`
/// attribute to customize deserialization of HashSet fields.
///
/// # Arguments
///
/// * `deserializer` - Serde deserializer instance
///
/// # Returns
///
/// * `Ok(HashSet<T>)` - Successfully deserialized HashSet
/// * `Err(D::Error)` - Serde deserialization error
///
/// # Type Parameters
///
/// * `D` - Serde deserializer type
/// * `T` - Target type that implements `FromStr`, `Eq`, and `Hash`
pub(crate) fn deserialize_hashset<'a, D, T>(deserializer: D) -> Result<HashSet<T>, D::Error>
where
D: serde::Deserializer<'a>,
T: Eq + std::str::FromStr + std::hash::Hash,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
use serde::de::Error;
deserialize_hashset_inner(<String>::deserialize(deserializer)?).map_err(D::Error::custom)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
#[test]
fn test_deserialize_hashset_inner_success() {
let result: Result<HashSet<i32>, String> = deserialize_hashset_inner("1,2,3");
assert!(result.is_ok());
if let Ok(hashset) = result {
assert_eq!(hashset.len(), 3);
assert!(hashset.contains(&1));
assert!(hashset.contains(&2));
assert!(hashset.contains(&3));
}
}
#[test]
fn test_deserialize_hashset_inner_with_whitespace() {
let result: Result<HashSet<String>, String> = deserialize_hashset_inner(" a , b , c ");
assert!(result.is_ok());
if let Ok(hashset) = result {
assert_eq!(hashset.len(), 3);
assert!(hashset.contains("a"));
assert!(hashset.contains("b"));
assert!(hashset.contains("c"));
}
}
#[test]
fn test_deserialize_hashset_inner_empty_string() {
let result: Result<HashSet<String>, String> = deserialize_hashset_inner("");
assert!(result.is_ok());
if let Ok(hashset) = result {
assert_eq!(hashset.len(), 0);
}
}
#[test]
fn test_deserialize_hashset_inner_single_value() {
let result: Result<HashSet<String>, String> = deserialize_hashset_inner("single");
assert!(result.is_ok());
if let Ok(hashset) = result {
assert_eq!(hashset.len(), 1);
assert!(hashset.contains("single"));
}
}
#[test]
fn test_deserialize_hashset_inner_invalid_int() {
let result: Result<HashSet<i32>, String> = deserialize_hashset_inner("1,invalid,3");
assert!(result.is_err());
if let Err(error) = result {
assert!(error.contains("Unable to deserialize `invalid` as `i32`"));
}
}
#[test]
fn test_deserialize_hashset_inner_duplicates() {
let result: Result<HashSet<String>, String> = deserialize_hashset_inner("a,b,a,c,b");
assert!(result.is_ok());
if let Ok(hashset) = result {
assert_eq!(hashset.len(), 3); // Duplicates should be removed
assert!(hashset.contains("a"));
assert!(hashset.contains("b"));
assert!(hashset.contains("c"));
}
}
}

View File

@ -1,5 +1,7 @@
use std::str::FromStr;
use api_models::admin; use api_models::admin;
use common_enums::{AttemptStatus, GatewaySystem, PaymentMethodType}; use common_enums::{connector_enums::Connector, AttemptStatus, GatewaySystem, PaymentMethodType};
use common_utils::{errors::CustomResult, ext_traits::ValueExt}; use common_utils::{errors::CustomResult, ext_traits::ValueExt};
use diesel_models::types::FeatureMetadata; use diesel_models::types::FeatureMetadata;
use error_stack::ResultExt; use error_stack::ResultExt;
@ -105,6 +107,9 @@ where
.get_string_repr(); .get_string_repr();
let connector_name = router_data.connector.clone(); let connector_name = router_data.connector.clone();
let connector_enum = Connector::from_str(&connector_name)
.change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven)?;
let payment_method = router_data.payment_method.to_string(); let payment_method = router_data.payment_method.to_string();
let flow_name = get_flow_name::<F>()?; let flow_name = get_flow_name::<F>()?;
@ -113,7 +118,7 @@ where
.grpc_client .grpc_client
.unified_connector_service .unified_connector_service
.as_ref() .as_ref()
.is_some_and(|config| config.ucs_only_connectors.contains(&connector_name)); .is_some_and(|config| config.ucs_only_connectors.contains(&connector_enum));
if is_ucs_only_connector { if is_ucs_only_connector {
router_env::logger::info!( router_env::logger::info!(