feat(ucs): add gateway system {Direct | UnifiedConnectorSystem} in feature metadata for v1 (#8854)

This commit is contained in:
Uzair Khan
2025-08-11 19:13:15 +05:30
committed by GitHub
parent 0e95785437
commit d034fadbdc
13 changed files with 313 additions and 19 deletions

View File

@ -2220,6 +2220,34 @@ pub enum PaymentMethod {
MobilePayment,
}
/// Indicates the gateway system through which the payment is processed.
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
PartialOrd,
Ord,
Hash,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::VariantNames,
strum::EnumIter,
strum::EnumString,
ToSchema,
)]
#[router_derive::diesel_enum(storage_type = "text")]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum GatewaySystem {
#[default]
Direct,
UnifiedConnectorService,
}
/// The type of the payment that differentiates between normal and various types of mandate payments. Use 'setup_mandate' in case of zero auth flow.
#[derive(
Clone,

View File

@ -2,6 +2,7 @@ use common_enums::{PaymentMethodType, RequestIncrementalAuthorization};
use common_types::primitive_wrappers::RequestExtendedAuthorizationBool;
use common_utils::{encryption::Encryption, pii, types::MinorUnit};
use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable};
use masking::ExposeInterface;
use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime;
@ -492,6 +493,7 @@ pub enum PaymentIntentUpdate {
fingerprint_id: Option<String>,
updated_by: String,
incremental_authorization_allowed: Option<bool>,
feature_metadata: Option<masking::Secret<serde_json::Value>>,
},
MetadataUpdate {
metadata: serde_json::Value,
@ -517,6 +519,7 @@ pub enum PaymentIntentUpdate {
status: storage_enums::IntentStatus,
updated_by: String,
incremental_authorization_allowed: Option<bool>,
feature_metadata: Option<masking::Secret<serde_json::Value>>,
},
PaymentAttemptAndAttemptCountUpdate {
active_attempt_id: String,
@ -625,6 +628,7 @@ pub struct PaymentIntentUpdateFields {
pub force_3ds_challenge: Option<bool>,
pub is_iframe_redirection_enabled: Option<bool>,
pub payment_channel: Option<common_enums::PaymentChannel>,
pub feature_metadata: Option<masking::Secret<serde_json::Value>>,
pub tax_status: Option<common_enums::TaxStatus>,
pub discount_amount: Option<MinorUnit>,
pub order_date: Option<PrimitiveDateTime>,
@ -845,6 +849,7 @@ pub struct PaymentIntentUpdateInternal {
pub is_iframe_redirection_enabled: Option<bool>,
pub extended_return_url: Option<String>,
pub payment_channel: Option<common_enums::PaymentChannel>,
pub feature_metadata: Option<masking::Secret<serde_json::Value>>,
pub tax_status: Option<common_enums::TaxStatus>,
pub discount_amount: Option<MinorUnit>,
pub order_date: Option<PrimitiveDateTime>,
@ -897,6 +902,7 @@ impl PaymentIntentUpdate {
is_iframe_redirection_enabled,
extended_return_url,
payment_channel,
feature_metadata,
tax_status,
discount_amount,
order_date,
@ -953,6 +959,9 @@ impl PaymentIntentUpdate {
.or(source.is_iframe_redirection_enabled),
extended_return_url: extended_return_url.or(source.extended_return_url),
payment_channel: payment_channel.or(source.payment_channel),
feature_metadata: feature_metadata
.map(|value| value.expose())
.or(source.feature_metadata),
tax_status: tax_status.or(source.tax_status),
discount_amount: discount_amount.or(source.discount_amount),
order_date: order_date.or(source.order_date),
@ -1013,6 +1022,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1062,6 +1072,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: value.is_iframe_redirection_enabled,
extended_return_url: value.return_url,
payment_channel: value.payment_channel,
feature_metadata: value.feature_metadata,
tax_status: value.tax_status,
discount_amount: value.discount_amount,
order_date: value.order_date,
@ -1118,6 +1129,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: return_url,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1129,6 +1141,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
status,
updated_by,
incremental_authorization_allowed,
feature_metadata,
} => Self {
status: Some(status),
modified_at: common_utils::date_time::now(),
@ -1170,6 +1183,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1223,6 +1237,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1239,6 +1254,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
// customer_id,
updated_by,
incremental_authorization_allowed,
feature_metadata,
} => Self {
// amount,
// currency: Some(currency),
@ -1283,6 +1299,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1335,6 +1352,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1388,6 +1406,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1440,6 +1459,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1492,6 +1512,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1543,6 +1564,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1591,6 +1613,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1641,6 +1664,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1691,6 +1715,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1739,6 +1764,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,
@ -1792,6 +1818,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
is_iframe_redirection_enabled: None,
extended_return_url: None,
payment_channel: None,
feature_metadata: None,
tax_status: None,
discount_amount: None,
order_date: None,

View File

@ -101,7 +101,7 @@ impl FeatureMetadata {
}
#[cfg(feature = "v1")]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, FromSqlRow, AsExpression)]
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, FromSqlRow, AsExpression)]
#[diesel(sql_type = Json)]
pub struct FeatureMetadata {
/// Redirection response coming in request as metadata field only for redirection scenarios
@ -110,6 +110,8 @@ pub struct FeatureMetadata {
pub search_tags: Option<Vec<HashedString<WithType>>>,
/// Recurring payment details required for apple pay Merchant Token
pub apple_pay_recurring_details: Option<ApplePayRecurringDetails>,
/// The system that the gateway is integrated with, e.g., `Direct`(through hyperswitch), `UnifiedConnectorService`(through ucs), etc.
pub gateway_system: Option<common_enums::GatewaySystem>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, FromSqlRow, AsExpression)]

View File

@ -186,7 +186,10 @@ impl UnifiedConnectorServiceClient {
}
}
}
None => None,
None => {
router_env::logger::error!(?config.unified_connector_service, "Unified Connector Service config is missing");
None
}
}
}

View File

@ -121,6 +121,7 @@ impl ApiModelToDieselModelConvertor<ApiFeatureMetadata> for FeatureMetadata {
search_tags,
apple_pay_recurring_details: apple_pay_recurring_details
.map(ApplePayRecurringDetails::convert_from),
gateway_system: None,
}
}
@ -129,6 +130,7 @@ impl ApiModelToDieselModelConvertor<ApiFeatureMetadata> for FeatureMetadata {
redirect_response,
search_tags,
apple_pay_recurring_details,
..
} = self;
ApiFeatureMetadata {

View File

@ -249,6 +249,7 @@ pub struct PaymentIntentUpdateFields {
pub duty_amount: Option<MinorUnit>,
pub is_confirm_operation: bool,
pub payment_channel: Option<common_enums::PaymentChannel>,
pub feature_metadata: Option<Secret<serde_json::Value>>,
pub enable_partial_authorization: Option<bool>,
}
@ -261,6 +262,7 @@ pub enum PaymentIntentUpdate {
updated_by: String,
fingerprint_id: Option<String>,
incremental_authorization_allowed: Option<bool>,
feature_metadata: Option<Secret<serde_json::Value>>,
},
MetadataUpdate {
metadata: serde_json::Value,
@ -286,6 +288,7 @@ pub enum PaymentIntentUpdate {
status: common_enums::IntentStatus,
incremental_authorization_allowed: Option<bool>,
updated_by: String,
feature_metadata: Option<Secret<serde_json::Value>>,
},
PaymentAttemptAndAttemptCountUpdate {
active_attempt_id: String,
@ -437,6 +440,7 @@ pub struct PaymentIntentUpdateInternal {
pub force_3ds_challenge: Option<bool>,
pub is_iframe_redirection_enabled: Option<bool>,
pub payment_channel: Option<common_enums::PaymentChannel>,
pub feature_metadata: Option<Secret<serde_json::Value>>,
pub tax_status: Option<common_enums::TaxStatus>,
pub discount_amount: Option<MinorUnit>,
pub order_date: Option<PrimitiveDateTime>,
@ -874,11 +878,13 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
status,
updated_by,
incremental_authorization_allowed,
feature_metadata,
} => Self {
status: Some(status),
modified_at: Some(common_utils::date_time::now()),
updated_by,
incremental_authorization_allowed,
feature_metadata,
..Default::default()
},
PaymentIntentUpdate::MerchantStatusUpdate {
@ -903,6 +909,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
// customer_id,
updated_by,
incremental_authorization_allowed,
feature_metadata,
} => Self {
// amount,
// currency: Some(currency),
@ -913,6 +920,7 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
modified_at: Some(common_utils::date_time::now()),
updated_by,
incremental_authorization_allowed,
feature_metadata,
..Default::default()
},
PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate {
@ -1034,12 +1042,14 @@ impl From<PaymentIntentUpdate> for DieselPaymentIntentUpdate {
fingerprint_id,
updated_by,
incremental_authorization_allowed,
feature_metadata,
} => Self::ResponseUpdate {
status,
amount_captured,
fingerprint_id,
updated_by,
incremental_authorization_allowed,
feature_metadata,
},
PaymentIntentUpdate::MetadataUpdate {
metadata,
@ -1081,6 +1091,7 @@ impl From<PaymentIntentUpdate> for DieselPaymentIntentUpdate {
force_3ds_challenge: value.force_3ds_challenge,
is_iframe_redirection_enabled: value.is_iframe_redirection_enabled,
payment_channel: value.payment_channel,
feature_metadata: value.feature_metadata,
tax_status: value.tax_status,
discount_amount: value.discount_amount,
order_date: value.order_date,
@ -1121,10 +1132,12 @@ impl From<PaymentIntentUpdate> for DieselPaymentIntentUpdate {
status,
updated_by,
incremental_authorization_allowed,
feature_metadata,
} => Self::PGStatusUpdate {
status,
updated_by,
incremental_authorization_allowed,
feature_metadata,
},
PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate {
active_attempt_id,
@ -1246,6 +1259,7 @@ impl From<PaymentIntentUpdateInternal> for diesel_models::PaymentIntentUpdateInt
force_3ds_challenge,
is_iframe_redirection_enabled,
payment_channel,
feature_metadata,
tax_status,
discount_amount,
order_date,
@ -1294,6 +1308,7 @@ impl From<PaymentIntentUpdateInternal> for diesel_models::PaymentIntentUpdateInt
is_iframe_redirection_enabled,
extended_return_url: return_url,
payment_channel,
feature_metadata,
tax_status,
discount_amount,
order_date,

View File

@ -34,7 +34,7 @@ use api_models::{
mandates::RecurringDetails,
payments::{self as payments_api},
};
pub use common_enums::enums::CallConnectorAction;
pub use common_enums::enums::{CallConnectorAction, GatewaySystem};
use common_types::payments as common_payments_types;
use common_utils::{
ext_traits::{AsyncExt, StringExt},
@ -91,6 +91,8 @@ use self::{
operations::{BoxedOperation, Operation, PaymentResponse},
routing::{self as self_routing, SessionFlowRoutingInput},
};
#[cfg(feature = "v1")]
use super::unified_connector_service::update_gateway_system_in_feature_metadata;
use super::{
errors::StorageErrorExt, payment_methods::surcharge_decision_configs, routing::TransactionData,
unified_connector_service::should_call_unified_connector_service,
@ -4043,7 +4045,22 @@ where
services::api::ConnectorIntegration<F, RouterDReq, router_types::PaymentsResponseData>,
{
record_time_taken_with(|| async {
if should_call_unified_connector_service(state, merchant_context, &router_data).await? {
if should_call_unified_connector_service(
state,
merchant_context,
&router_data,
Some(payment_data),
)
.await?
{
router_env::logger::info!(
"Processing payment through UCS gateway system - payment_id={}, attempt_id={}",
payment_data
.get_payment_intent()
.payment_id
.get_string_repr(),
payment_data.get_payment_attempt().attempt_id
);
if should_add_task_to_process_tracker(payment_data) {
operation
.to_domain()?
@ -4058,6 +4075,12 @@ where
.ok();
}
// Update feature metadata to track UCS usage for stickiness
update_gateway_system_in_feature_metadata(
payment_data,
GatewaySystem::UnifiedConnectorService,
)?;
(_, *payment_data) = operation
.to_update_tracker()?
.update_trackers(
@ -4083,6 +4106,18 @@ where
Ok((router_data, merchant_connector_account))
} else {
router_env::logger::info!(
"Processing payment through Direct gateway system - payment_id={}, attempt_id={}",
payment_data
.get_payment_intent()
.payment_id
.get_string_repr(),
payment_data.get_payment_attempt().attempt_id
);
// Update feature metadata to track Direct routing usage for stickiness
update_gateway_system_in_feature_metadata(payment_data, GatewaySystem::Direct)?;
call_connector_service(
state,
req_state,
@ -4440,8 +4475,13 @@ where
.await?;
// do order creation
let should_call_unified_connector_service =
should_call_unified_connector_service(state, merchant_context, &router_data).await?;
let should_call_unified_connector_service = should_call_unified_connector_service(
state,
merchant_context,
&router_data,
Some(payment_data),
)
.await?;
let (connector_request, should_continue_further) = if !should_call_unified_connector_service {
let mut should_continue_further = true;
@ -4501,6 +4541,12 @@ where
record_time_taken_with(|| async {
if should_call_unified_connector_service {
router_env::logger::info!(
"Processing payment through UCS gateway system- payment_id={}, attempt_id={}",
payment_data.get_payment_intent().id.get_string_repr(),
payment_data.get_payment_attempt().id.get_string_repr()
);
router_data
.call_unified_connector_service(
state,
@ -4556,7 +4602,19 @@ where
services::api::ConnectorIntegration<F, RouterDReq, router_types::PaymentsResponseData>,
{
record_time_taken_with(|| async {
if should_call_unified_connector_service(state, merchant_context, &router_data).await? {
if should_call_unified_connector_service(
state,
merchant_context,
&router_data,
Some(payment_data),
)
.await?
{
router_env::logger::info!(
"Executing payment through UCS gateway system - payment_id={}, attempt_id={}",
payment_data.get_payment_intent().id.get_string_repr(),
payment_data.get_payment_attempt().id.get_string_repr()
);
if should_add_task_to_process_tracker(payment_data) {
operation
.to_domain()?
@ -4596,6 +4654,12 @@ where
Ok(router_data)
} else {
router_env::logger::info!(
"Processing payment through Direct gateway system - payment_id={}, attempt_id={}",
payment_data.get_payment_intent().id.get_string_repr(),
payment_data.get_payment_attempt().id.get_string_repr()
);
call_connector_service(
state,
req_state,

View File

@ -256,6 +256,11 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsCancelReques
status: enums::IntentStatus::Cancelled,
updated_by: storage_scheme.to_string(),
incremental_authorization_allowed: None,
feature_metadata: payment_data
.payment_intent
.feature_metadata
.clone()
.map(masking::Secret::new),
};
(Some(payment_intent_update), enums::AttemptStatus::Voided)
} else {

View File

@ -2056,6 +2056,11 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for
.is_iframe_redirection_enabled,
is_confirm_operation: true, // Indicates that this is a confirm operation
payment_channel: payment_data.payment_intent.payment_channel,
feature_metadata: payment_data
.payment_intent
.feature_metadata
.clone()
.map(masking::Secret::new),
tax_status: payment_data.payment_intent.tax_status,
discount_amount: payment_data.payment_intent.discount_amount,
order_date: payment_data.payment_intent.order_date,

View File

@ -2113,6 +2113,11 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
updated_by: storage_scheme.to_string(),
// make this false only if initial payment fails, if incremental authorization call fails don't make it false
incremental_authorization_allowed: Some(false),
feature_metadata: payment_data
.payment_intent
.feature_metadata
.clone()
.map(masking::Secret::new),
},
Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate {
status: api_models::enums::IntentStatus::foreign_from(
@ -2124,6 +2129,11 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
incremental_authorization_allowed: payment_data
.payment_intent
.incremental_authorization_allowed,
feature_metadata: payment_data
.payment_intent
.feature_metadata
.clone()
.map(masking::Secret::new),
},
};

View File

@ -953,6 +953,11 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for
.is_iframe_redirection_enabled,
is_confirm_operation: false, // this is not a confirm operation
payment_channel: payment_data.payment_intent.payment_channel,
feature_metadata: payment_data
.payment_intent
.feature_metadata
.clone()
.map(masking::Secret::new),
tax_status: payment_data.payment_intent.tax_status,
discount_amount: payment_data.payment_intent.discount_amount,
order_date: payment_data.payment_intent.order_date,

View File

@ -1,6 +1,7 @@
use api_models::admin;
use common_enums::{AttemptStatus, PaymentMethodType};
use common_enums::{AttemptStatus, GatewaySystem, PaymentMethodType};
use common_utils::{errors::CustomResult, ext_traits::ValueExt};
use diesel_models::types::FeatureMetadata;
use error_stack::ResultExt;
use external_services::grpc_client::unified_connector_service::{
ConnectorAuthMetadata, UnifiedConnectorServiceError,
@ -23,10 +24,13 @@ use unified_connector_service_client::payments::{
use crate::{
consts,
core::{
errors::{ApiErrorResponse, RouterResult},
payments::helpers::{
errors::{self, RouterResult},
payments::{
helpers::{
is_ucs_enabled, should_execute_based_on_rollout, MerchantConnectorAccountType,
},
OperationSessionGetters, OperationSessionSetters,
},
utils::get_flow_name,
},
routes::SessionState,
@ -39,21 +43,62 @@ pub mod transformers;
// Re-export webhook transformer types for easier access
pub use transformers::WebhookTransformData;
pub async fn should_call_unified_connector_service<F: Clone, T>(
/// Generic version of should_call_unified_connector_service that works with any type
/// implementing OperationSessionGetters trait
pub async fn should_call_unified_connector_service<F: Clone, T, D>(
state: &SessionState,
merchant_context: &MerchantContext,
router_data: &RouterData<F, T, PaymentsResponseData>,
) -> RouterResult<bool> {
payment_data: Option<&D>,
) -> RouterResult<bool>
where
D: OperationSessionGetters<F>,
{
// Check basic UCS availability first
if state.grpc_client.unified_connector_service_client.is_none() {
router_env::logger::debug!(
"Unified Connector Service client is not available, skipping UCS decision"
);
return Ok(false);
}
let ucs_config_key = consts::UCS_ENABLED;
if !is_ucs_enabled(state, ucs_config_key).await {
router_env::logger::debug!(
"Unified Connector Service is not enabled, skipping UCS decision"
);
return Ok(false);
}
// Apply stickiness logic if payment_data is available
if let Some(payment_data) = payment_data {
let previous_gateway_system = extract_gateway_system_from_payment_intent(payment_data);
match previous_gateway_system {
Some(GatewaySystem::UnifiedConnectorService) => {
// Payment intent previously used UCS, maintain stickiness to UCS
router_env::logger::info!(
"Payment gateway system decision: UCS (sticky) - payment intent previously used UCS"
);
return Ok(true);
}
Some(GatewaySystem::Direct) => {
// Payment intent previously used Direct, maintain stickiness to Direct (return false for UCS)
router_env::logger::info!(
"Payment gateway system decision: Direct (sticky) - payment intent previously used Direct"
);
return Ok(false);
}
None => {
// No previous gateway system set, continue with normal gateway system logic
router_env::logger::debug!(
"UCS stickiness: No previous gateway system set, applying normal gateway system logic"
);
}
}
}
// Continue with normal UCS gateway system logic
let merchant_id = merchant_context
.get_merchant_account()
.get_id()
@ -71,8 +116,13 @@ pub async fn should_call_unified_connector_service<F: Clone, T>(
.is_some_and(|config| config.ucs_only_connectors.contains(&connector_name));
if is_ucs_only_connector {
router_env::logger::info!(
"Payment gateway system decision: UCS (forced) - merchant_id={}, connector={}, payment_method={}, flow={}",
merchant_id, connector_name, payment_method, flow_name
);
return Ok(true);
}
let config_key = format!(
"{}_{}_{}_{}_{}",
consts::UCS_ROLLOUT_PERCENT_CONFIG_PREFIX,
@ -83,9 +133,87 @@ pub async fn should_call_unified_connector_service<F: Clone, T>(
);
let should_execute = should_execute_based_on_rollout(state, &config_key).await?;
// Log gateway system decision
if should_execute {
router_env::logger::info!(
"Payment gateway system decision: UCS - merchant_id={}, connector={}, payment_method={}, flow={}",
merchant_id, connector_name, payment_method, flow_name
);
} else {
router_env::logger::info!(
"Payment gateway system decision: Direct - merchant_id={}, connector={}, payment_method={}, flow={}",
merchant_id, connector_name, payment_method, flow_name
);
}
Ok(should_execute)
}
/// Extracts the gateway system from the payment intent's feature metadata
/// Returns None if metadata is missing, corrupted, or doesn't contain gateway_system
fn extract_gateway_system_from_payment_intent<F: Clone, D>(
payment_data: &D,
) -> Option<GatewaySystem>
where
D: OperationSessionGetters<F>,
{
#[cfg(feature = "v1")]
{
payment_data
.get_payment_intent()
.feature_metadata
.as_ref()
.and_then(|metadata| {
// Try to parse the JSON value as FeatureMetadata
// Log errors but don't fail the flow for corrupted metadata
match serde_json::from_value::<FeatureMetadata>(metadata.clone()) {
Ok(feature_metadata) => feature_metadata.gateway_system,
Err(err) => {
router_env::logger::warn!(
"Failed to parse feature_metadata for gateway_system extraction: {}",
err
);
None
}
}
})
}
#[cfg(feature = "v2")]
{
None // V2 does not use feature metadata for gateway system tracking
}
}
/// Updates the payment intent's feature metadata to track the gateway system being used
#[cfg(feature = "v1")]
pub fn update_gateway_system_in_feature_metadata<F: Clone, D>(
payment_data: &mut D,
gateway_system: GatewaySystem,
) -> RouterResult<()>
where
D: OperationSessionGetters<F> + OperationSessionSetters<F>,
{
let mut payment_intent = payment_data.get_payment_intent().clone();
let existing_metadata = payment_intent.feature_metadata.as_ref();
let mut feature_metadata = existing_metadata
.and_then(|metadata| serde_json::from_value::<FeatureMetadata>(metadata.clone()).ok())
.unwrap_or_default();
feature_metadata.gateway_system = Some(gateway_system);
let updated_metadata = serde_json::to_value(feature_metadata)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to serialize feature metadata")?;
payment_intent.feature_metadata = Some(updated_metadata.clone());
payment_data.set_payment_intent(payment_intent);
Ok(())
}
pub async fn should_call_unified_connector_service_for_webhooks(
state: &SessionState,
merchant_context: &MerchantContext,
@ -422,7 +550,7 @@ pub async fn call_unified_connector_service_for_webhook(
.unified_connector_service_client
.as_ref()
.ok_or_else(|| {
error_stack::report!(ApiErrorResponse::WebhookProcessingFailure)
error_stack::report!(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("UCS client is not available for webhook processing")
})?;
@ -472,10 +600,10 @@ pub async fn call_unified_connector_service_for_webhook(
build_unified_connector_service_auth_metadata(mca_type, merchant_context)
})
.transpose()
.change_context(ApiErrorResponse::InternalServerError)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to build UCS auth metadata")?
.ok_or_else(|| {
error_stack::report!(ApiErrorResponse::InternalServerError).attach_printable(
error_stack::report!(errors::ApiErrorResponse::InternalServerError).attach_printable(
"Missing merchant connector account for UCS webhook transformation",
)
})?;
@ -504,7 +632,7 @@ pub async fn call_unified_connector_service_for_webhook(
}
Err(err) => {
// When UCS is configured, we don't fall back to direct connector processing
Err(ApiErrorResponse::WebhookProcessingFailure)
Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable(format!("UCS webhook processing failed: {err}"))
}
}

View File

@ -139,7 +139,7 @@ impl ProcessTrackerWorkflow<SessionState> for PaymentsSyncWorkflow {
.as_ref()
.is_none()
{
let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string(), incremental_authorization_allowed: Some(false) };
let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string(), incremental_authorization_allowed: Some(false), feature_metadata: payment_data.payment_intent.feature_metadata.clone().map(masking::Secret::new), };
let payment_attempt_update =
hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ErrorUpdate {
connector: None,