feat(core): [proxy payments] send external vault proxy metadata to UCS (#9108)

This commit is contained in:
Sakil Mostak
2025-09-01 16:28:37 +05:30
committed by GitHub
parent ff14b7cac8
commit c02d8b9ba9
8 changed files with 244 additions and 17 deletions

View File

@ -156,6 +156,23 @@ pub struct ConnectorAuthMetadata {
pub merchant_id: Secret<String>,
}
/// External Vault Proxy Related Metadata
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum ExternalVaultProxyMetadata {
/// VGS proxy data variant
VgsMetadata(VgsMetadata),
}
/// VGS proxy data
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct VgsMetadata {
/// External vault url
pub proxy_url: Url,
/// CA certificates to verify the vault server
pub certificate: Secret<String>,
}
impl UnifiedConnectorServiceClient {
/// Builds the connection to the gRPC service
pub async fn build_connections(config: &GrpcClientSettings) -> Option<Self> {
@ -206,13 +223,18 @@ impl UnifiedConnectorServiceClient {
&self,
payment_authorize_request: payments_grpc::PaymentServiceAuthorizeRequest,
connector_auth_metadata: ConnectorAuthMetadata,
external_vault_proxy_metadata: Option<String>,
grpc_headers: GrpcHeaders,
) -> UnifiedConnectorServiceResult<tonic::Response<PaymentServiceAuthorizeResponse>> {
let mut request = tonic::Request::new(payment_authorize_request);
let connector_name = connector_auth_metadata.connector_name.clone();
let metadata =
build_unified_connector_service_grpc_headers(connector_auth_metadata, grpc_headers)?;
let metadata = build_unified_connector_service_grpc_headers(
connector_auth_metadata,
external_vault_proxy_metadata,
grpc_headers,
)?;
*request.metadata_mut() = metadata;
self.client
@ -241,8 +263,11 @@ impl UnifiedConnectorServiceClient {
let mut request = tonic::Request::new(payment_get_request);
let connector_name = connector_auth_metadata.connector_name.clone();
let metadata =
build_unified_connector_service_grpc_headers(connector_auth_metadata, grpc_headers)?;
let metadata = build_unified_connector_service_grpc_headers(
connector_auth_metadata,
None,
grpc_headers,
)?;
*request.metadata_mut() = metadata;
self.client
@ -271,8 +296,11 @@ impl UnifiedConnectorServiceClient {
let mut request = tonic::Request::new(payment_register_request);
let connector_name = connector_auth_metadata.connector_name.clone();
let metadata =
build_unified_connector_service_grpc_headers(connector_auth_metadata, grpc_headers)?;
let metadata = build_unified_connector_service_grpc_headers(
connector_auth_metadata,
None,
grpc_headers,
)?;
*request.metadata_mut() = metadata;
self.client
@ -302,8 +330,11 @@ impl UnifiedConnectorServiceClient {
let mut request = tonic::Request::new(payment_repeat_request);
let connector_name = connector_auth_metadata.connector_name.clone();
let metadata =
build_unified_connector_service_grpc_headers(connector_auth_metadata, grpc_headers)?;
let metadata = build_unified_connector_service_grpc_headers(
connector_auth_metadata,
None,
grpc_headers,
)?;
*request.metadata_mut() = metadata;
self.client
@ -331,8 +362,11 @@ impl UnifiedConnectorServiceClient {
let mut request = tonic::Request::new(webhook_transform_request);
let connector_name = connector_auth_metadata.connector_name.clone();
let metadata =
build_unified_connector_service_grpc_headers(connector_auth_metadata, grpc_headers)?;
let metadata = build_unified_connector_service_grpc_headers(
connector_auth_metadata,
None,
grpc_headers,
)?;
*request.metadata_mut() = metadata;
self.client
@ -354,6 +388,7 @@ impl UnifiedConnectorServiceClient {
/// Build the gRPC Headers for Unified Connector Service Request
pub fn build_unified_connector_service_grpc_headers(
meta: ConnectorAuthMetadata,
external_vault_proxy_metadata: Option<String>,
grpc_headers: GrpcHeaders,
) -> Result<MetadataMap, UnifiedConnectorServiceError> {
let mut metadata = MetadataMap::new();
@ -405,6 +440,13 @@ pub fn build_unified_connector_service_grpc_headers(
parse(common_utils_consts::X_MERCHANT_ID, meta.merchant_id.peek())?,
);
if let Some(external_vault_proxy_metadata) = external_vault_proxy_metadata {
metadata.append(
consts::UCS_HEADER_EXTERNAL_VAULT_METADATA,
parse("external_vault_metadata", &external_vault_proxy_metadata)?,
);
};
if let Err(err) = grpc_headers
.tenant_id
.parse()

View File

@ -91,6 +91,9 @@ pub mod consts {
/// Header key for sending the AUTH KEY MAP in currency-based authentication.
pub(crate) const UCS_HEADER_AUTH_KEY_MAP: &str = "x-auth-key-map";
/// Header key for sending the EXTERNAL VAULT METADATA in proxy payments
pub(crate) const UCS_HEADER_EXTERNAL_VAULT_METADATA: &str = "x-external-vault-metadata";
}
/// Metrics for interactions with external systems.

View File

@ -312,6 +312,12 @@ pub struct RevenueRecoveryMetadata {
pub mca_reference: AccountReferenceMap,
}
#[cfg(feature = "v2")]
#[derive(Debug, Clone, serde::Deserialize)]
pub struct ExternalVaultConnectorMetadata {
pub proxy_url: common_utils::types::Url,
pub certificate: Secret<String>,
}
#[cfg(feature = "v2")]
#[derive(Debug, Clone)]
pub struct AccountReferenceMap {

View File

@ -1576,8 +1576,8 @@ where
let payment_data = match connector {
ConnectorCallType::PreDetermined(connector_data) => {
let (mca_type_details, updated_customer, router_data) =
call_connector_service_prerequisites(
let (mca_type_details, external_vault_mca_type_details, updated_customer, router_data) =
call_connector_service_prerequisites_for_external_vault_proxy(
state,
req_state.clone(),
&merchant_context,
@ -1614,6 +1614,7 @@ where
false, //should_retry_with_pan is set to false in case of PreDetermined ConnectorCallType
req.should_return_raw_response(),
mca_type_details,
external_vault_mca_type_details,
router_data,
updated_customer,
)
@ -4595,6 +4596,94 @@ where
))
}
#[cfg(feature = "v2")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
#[instrument(skip_all)]
pub async fn call_connector_service_prerequisites_for_external_vault_proxy<
F,
RouterDReq,
ApiRequest,
D,
>(
state: &SessionState,
req_state: ReqState,
merchant_context: &domain::MerchantContext,
connector: api::ConnectorData,
operation: &BoxedOperation<'_, F, ApiRequest, D>,
payment_data: &mut D,
customer: &Option<domain::Customer>,
call_connector_action: CallConnectorAction,
schedule_time: Option<time::PrimitiveDateTime>,
header_payload: HeaderPayload,
frm_suggestion: Option<storage_enums::FrmSuggestion>,
business_profile: &domain::Profile,
is_retry_payment: bool,
should_retry_with_pan: bool,
all_keys_required: Option<bool>,
) -> RouterResult<(
domain::MerchantConnectorAccountTypeDetails,
domain::MerchantConnectorAccountTypeDetails,
Option<storage::CustomerUpdate>,
RouterData<F, RouterDReq, router_types::PaymentsResponseData>,
)>
where
F: Send + Clone + Sync,
RouterDReq: Send + Sync,
// To create connector flow specific interface data
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
D: ConstructFlowSpecificData<F, RouterDReq, router_types::PaymentsResponseData>,
RouterData<F, RouterDReq, router_types::PaymentsResponseData>: Feature<F, RouterDReq> + Send,
// To construct connector flow specific api
dyn api::Connector:
services::api::ConnectorIntegration<F, RouterDReq, router_types::PaymentsResponseData>,
{
// get merchant connector account related to external vault
let external_vault_source: id_type::MerchantConnectorAccountId = business_profile
.external_vault_connector_details
.clone()
.map(|connector_details| connector_details.vault_connector_id.clone())
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("mca_id not present for external vault")?;
let external_vault_merchant_connector_account_type_details =
domain::MerchantConnectorAccountTypeDetails::MerchantConnectorAccount(Box::new(
helpers::get_merchant_connector_account_v2(
state,
merchant_context.get_merchant_key_store(),
Some(&external_vault_source),
)
.await?,
));
let (merchant_connector_account_type_details, updated_customer, router_data) =
call_connector_service_prerequisites(
state,
req_state,
merchant_context,
connector,
operation,
payment_data,
customer,
call_connector_action,
schedule_time,
header_payload,
frm_suggestion,
business_profile,
is_retry_payment,
should_retry_with_pan,
all_keys_required,
)
.await?;
Ok((
merchant_connector_account_type_details,
external_vault_merchant_connector_account_type_details,
updated_customer,
router_data,
))
}
#[cfg(feature = "v2")]
#[instrument(skip_all)]
pub async fn internal_call_connector_service_prerequisites<F, RouterDReq, ApiRequest, D>(
@ -4930,6 +5019,7 @@ pub async fn call_unified_connector_service_for_external_proxy<F, RouterDReq, Ap
_should_retry_with_pan: bool,
_return_raw_connector_response: Option<bool>,
merchant_connector_account_type_details: domain::MerchantConnectorAccountTypeDetails,
external_vault_merchant_connector_account_type_details: domain::MerchantConnectorAccountTypeDetails,
mut router_data: RouterData<F, RouterDReq, router_types::PaymentsResponseData>,
_updated_customer: Option<storage::CustomerUpdate>,
) -> RouterResult<RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
@ -4962,9 +5052,10 @@ where
.await?;
router_data
.call_unified_connector_service(
.call_unified_connector_service_with_external_vault_proxy(
state,
merchant_connector_account_type_details.clone(),
external_vault_merchant_connector_account_type_details.clone(),
merchant_context,
)
.await?;

View File

@ -218,6 +218,22 @@ pub trait Feature<F, T> {
{
Ok(())
}
#[cfg(feature = "v2")]
async fn call_unified_connector_service_with_external_vault_proxy<'a>(
&mut self,
_state: &SessionState,
_merchant_connector_account: domain::MerchantConnectorAccountTypeDetails,
_external_vault_merchant_connector_account: domain::MerchantConnectorAccountTypeDetails,
_merchant_context: &domain::MerchantContext,
) -> RouterResult<()>
where
F: Clone,
Self: Sized,
dyn api::Connector: services::ConnectorIntegration<F, T, types::PaymentsResponseData>,
{
Ok(())
}
}
/// Determines whether a capture API call should be made for a payment attempt

View File

@ -858,6 +858,7 @@ async fn call_unified_connector_service_authorize(
.payment_authorize(
payment_authorize_request,
connector_auth_metadata,
None,
state.get_grpc_headers(),
)
.await

View File

@ -359,12 +359,12 @@ impl Feature<api::ExternalVaultProxy, types::ExternalVaultProxyPaymentsData>
}
}
async fn call_unified_connector_service<'a>(
#[cfg(feature = "v2")]
async fn call_unified_connector_service_with_external_vault_proxy<'a>(
&mut self,
state: &SessionState,
#[cfg(feature = "v1")] merchant_connector_account: helpers::MerchantConnectorAccountType,
#[cfg(feature = "v2")]
merchant_connector_account: domain::MerchantConnectorAccountTypeDetails,
external_vault_merchant_connector_account: domain::MerchantConnectorAccountTypeDetails,
merchant_context: &domain::MerchantContext,
) -> RouterResult<()> {
let client = state
@ -387,10 +387,18 @@ impl Feature<api::ExternalVaultProxy, types::ExternalVaultProxyPaymentsData>
.change_context(ApiErrorResponse::InternalServerError)
.attach_printable("Failed to construct request metadata")?;
let external_vault_proxy_metadata =
unified_connector_service::build_unified_connector_service_external_vault_proxy_metadata(
external_vault_merchant_connector_account
)
.change_context(ApiErrorResponse::InternalServerError)
.attach_printable("Failed to construct external vault proxy metadata")?;
let response = client
.payment_authorize(
payment_authorize_request,
connector_auth_metadata,
Some(external_vault_proxy_metadata),
state.get_grpc_headers(),
)
.await

View File

@ -1,7 +1,11 @@
use std::str::FromStr;
use api_models::admin;
#[cfg(feature = "v2")]
use base64::Engine;
use common_enums::{connector_enums::Connector, AttemptStatus, GatewaySystem, PaymentMethodType};
#[cfg(feature = "v2")]
use common_utils::consts::BASE64_ENGINE;
use common_utils::{errors::CustomResult, ext_traits::ValueExt};
use diesel_models::types::FeatureMetadata;
use error_stack::ResultExt;
@ -10,7 +14,9 @@ use external_services::grpc_client::unified_connector_service::{
};
use hyperswitch_connectors::utils::CardData;
#[cfg(feature = "v2")]
use hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccountTypeDetails;
use hyperswitch_domain_models::merchant_connector_account::{
ExternalVaultConnectorMetadata, MerchantConnectorAccountTypeDetails,
};
use hyperswitch_domain_models::{
merchant_context::MerchantContext,
router_data::{ConnectorAuthType, ErrorResponse, RouterData},
@ -24,6 +30,8 @@ use unified_connector_service_client::payments::{
PaymentServiceAuthorizeResponse, RewardPaymentMethodType,
};
#[cfg(feature = "v2")]
use crate::types::api::enums as api_enums;
use crate::{
consts,
core::{
@ -507,6 +515,58 @@ pub fn build_unified_connector_service_auth_metadata(
}
}
#[cfg(feature = "v2")]
pub fn build_unified_connector_service_external_vault_proxy_metadata(
external_vault_merchant_connector_account: MerchantConnectorAccountTypeDetails,
) -> CustomResult<String, UnifiedConnectorServiceError> {
let external_vault_metadata = external_vault_merchant_connector_account
.get_metadata()
.ok_or(UnifiedConnectorServiceError::ParsingFailed)
.attach_printable("Failed to obtain ConnectorMetadata")?;
let connector_name = external_vault_merchant_connector_account
.get_connector_name()
.map(|connector| connector.to_string())
.ok_or(UnifiedConnectorServiceError::MissingConnectorName)
.attach_printable("Missing connector name")?; // always get the connector name from this call
let external_vault_connector = api_enums::VaultConnectors::from_str(&connector_name)
.change_context(UnifiedConnectorServiceError::InvalidConnectorName)
.attach_printable("Failed to parse Vault connector")?;
let unified_service_vault_metdata = match external_vault_connector {
api_enums::VaultConnectors::Vgs => {
let vgs_metadata: ExternalVaultConnectorMetadata = external_vault_metadata
.expose()
.parse_value("ExternalVaultConnectorMetadata")
.change_context(UnifiedConnectorServiceError::ParsingFailed)
.attach_printable("Failed to parse Vgs connector metadata")?;
Some(external_services::grpc_client::unified_connector_service::ExternalVaultProxyMetadata::VgsMetadata(
external_services::grpc_client::unified_connector_service::VgsMetadata {
proxy_url: vgs_metadata.proxy_url,
certificate: vgs_metadata.certificate,
}
))
}
api_enums::VaultConnectors::HyperswitchVault => None,
};
match unified_service_vault_metdata {
Some(metdata) => {
let external_vault_metadata_bytes = serde_json::to_vec(&metdata)
.change_context(UnifiedConnectorServiceError::ParsingFailed)
.attach_printable("Failed to convert External vault metadata to bytes")?;
Ok(BASE64_ENGINE.encode(&external_vault_metadata_bytes))
}
None => Err(UnifiedConnectorServiceError::NotImplemented(
"External vault proxy metadata is not supported for {connector_name}".to_string(),
)
.into()),
}
}
pub fn handle_unified_connector_service_response_for_payment_authorize(
response: PaymentServiceAuthorizeResponse,
) -> CustomResult<