From 9f055e10a2793a01364933054ff66c28a5eca6da Mon Sep 17 00:00:00 2001 From: Uzair Khan <29498864+maverox@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:24:27 +0530 Subject: [PATCH] refactor(config): change UCS connector list from array to comma-separated string (#8905) --- config/config.example.toml | 1 + config/deployments/env_specific.toml | 1 + config/deployments/integration_test.toml | 3 + config/deployments/production.toml | 3 + config/deployments/sandbox.toml | 3 + config/development.toml | 7 +- .../grpc_client/unified_connector_service.rs | 10 +- crates/external_services/src/lib.rs | 3 + crates/external_services/src/utils.rs | 178 ++++++++++++++++++ .../src/core/unified_connector_service.rs | 9 +- 10 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 crates/external_services/src/utils.rs diff --git a/config/config.example.toml b/config/config.example.toml index d4ce454edf..5783a70c37 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1159,6 +1159,7 @@ url = "http://localhost:8080" # Open Router URL [grpc_client.unified_connector_service] base_url = "http://localhost:8000" # Unified Connector Service Base URL 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 base_url = "http://127.0.0.1:8080" #Base URL diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 6acf697ac4..64db7079f4 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -384,6 +384,7 @@ connector_names = "connector_names" # Comma-separated list of allowed connec [grpc_client.unified_connector_service] base_url = "http://localhost:8000" # Unified Connector Service Base URL 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 initial_timestamp_in_hours = 1 # number of hours added to start time for Decider service of Revenue Recovery diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 47c2bc39bc..a387cd0c16 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -837,3 +837,6 @@ click_to_pay = {connector_list = "adyen, cybersource, trustpay"} [list_dispute_supported_connectors] connector_list = "worldpayvantiv" + +[grpc_client.unified_connector_service] +ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 3550b1ec92..a95d88dc63 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -847,3 +847,6 @@ click_to_pay = {connector_list = "adyen, cybersource, trustpay"} [revenue_recovery] monitoring_threshold_in_seconds = 60 retry_algorithm_type = "cascading" + +[grpc_client.unified_connector_service] +ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 47a64b91b4..296888abcd 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -856,3 +856,6 @@ retry_algorithm_type = "cascading" [list_dispute_supported_connectors] connector_list = "worldpayvantiv" + +[grpc_client.unified_connector_service] +ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only diff --git a/config/development.toml b/config/development.toml index 7bc0dcc397..95b2f28962 100644 --- a/config/development.toml +++ b/config/development.toml @@ -1268,12 +1268,7 @@ enabled = "true" [grpc_client.unified_connector_service] base_url = "http://localhost:8000" connection_timeout = 10 -ucs_only_connectors = [ - "razorpay", - "phonepe", - "paytm", - "cashfree", -] +ucs_only_connectors = "paytm, phonepe" # Comma-separated list of connectors that use UCS only [revenue_recovery] monitoring_threshold_in_seconds = 60 diff --git a/crates/external_services/src/grpc_client/unified_connector_service.rs b/crates/external_services/src/grpc_client/unified_connector_service.rs index 62f681a985..81bb2dc898 100644 --- a/crates/external_services/src/grpc_client/unified_connector_service.rs +++ b/crates/external_services/src/grpc_client/unified_connector_service.rs @@ -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 error_stack::ResultExt; use masking::{PeekInterface, Secret}; @@ -18,6 +19,7 @@ use unified_connector_service_client::payments::{ use crate::{ consts, grpc_client::{GrpcClientSettings, GrpcHeaders}, + utils::deserialize_hashset, }; /// Unified Connector Service error variants @@ -123,9 +125,9 @@ pub struct UnifiedConnectorServiceClientConfig { /// Contains the connection timeout duration in seconds pub connection_timeout: u64, - /// List of connectors to use with the unified connector service - #[serde(default)] - pub ucs_only_connectors: Vec, + /// Set of external services/connectors available for the unified connector service + #[serde(default, deserialize_with = "deserialize_hashset")] + pub ucs_only_connectors: HashSet, } /// Contains the Connector Auth Type and related authentication data. diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index 7d97b5e99c..aee7d232bb 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -28,6 +28,9 @@ pub mod managers; /// crm module pub mod crm; +/// deserializers module_path +pub mod utils; + #[cfg(feature = "revenue_recovery")] /// date_time module pub mod date_time { diff --git a/crates/external_services/src/utils.rs b/crates/external_services/src/utils.rs new file mode 100644 index 0000000000..16cfc8ea00 --- /dev/null +++ b/crates/external_services/src/utils.rs @@ -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)` - 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, 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(value: impl AsRef) -> Result, String> +where + T: Eq + std::str::FromStr + std::hash::Hash, + ::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::() + ) + }) + }) + .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)` - 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, D::Error> +where + D: serde::Deserializer<'a>, + T: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, +{ + use serde::de::Error; + + deserialize_hashset_inner(::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, 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, 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, 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, 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, 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, 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")); + } + } +} diff --git a/crates/router/src/core/unified_connector_service.rs b/crates/router/src/core/unified_connector_service.rs index b14811ea73..2ef6e81eed 100644 --- a/crates/router/src/core/unified_connector_service.rs +++ b/crates/router/src/core/unified_connector_service.rs @@ -1,5 +1,7 @@ +use std::str::FromStr; + 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 diesel_models::types::FeatureMetadata; use error_stack::ResultExt; @@ -105,6 +107,9 @@ where .get_string_repr(); 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 flow_name = get_flow_name::()?; @@ -113,7 +118,7 @@ where .grpc_client .unified_connector_service .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 { router_env::logger::info!(