From ceb7d47da3988beaa04473d2175c8b46e93cb627 Mon Sep 17 00:00:00 2001 From: Gopikrishna C Date: Fri, 3 Jul 2026 11:48:26 +0530 Subject: [PATCH 1/3] fix(payouts): route merchant-authentication (access-token) calls through the payout connector During the merchant-authentication (access-token) flow, the access-token gateway unconditionally used the payment gRPC header builder (x-connector) and a payment reference, even when the caller was a payout. For payout access-token calls this selected the payment connector variant and carried a placeholder payment reference instead of the real payout reference. Make the access-token path payout-aware: - external_services: add create_access_token_for_payouts, which uses build_unified_connector_service_grpc_headers_for_payouts (x-payout-connector). - gateway: when router_data.payout_id is set, build the payout reference and resource ids and call the payout sibling. Payments and the existing payout_transfer/payout_get paths are untouched. --- .../grpc_client/unified_connector_service.rs | 43 ++++++++++ .../payments/gateway/access_token_gateway.rs | 82 ++++++++++++++----- 2 files changed, 103 insertions(+), 22 deletions(-) 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 e5e3e1b21e3..7cd3ef1cb4b 100644 --- a/crates/external_services/src/grpc_client/unified_connector_service.rs +++ b/crates/external_services/src/grpc_client/unified_connector_service.rs @@ -1083,6 +1083,49 @@ impl UnifiedConnectorServiceClient { }) } + /// Performs Create Access Token Granular for payouts + /// + /// Identical to [`Self::create_access_token`] but attaches the payout connector + /// header (`x-payout-connector`) so the merchant-authentication flow resolves the + /// payout connector variant instead of the payment one. + pub async fn create_access_token_for_payouts( + &self, + create_access_token_request: payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest, + connector_auth_metadata: ConnectorAuthMetadata, + grpc_headers: GrpcHeadersUcs, + ) -> UnifiedConnectorServiceResult< + tonic::Response< + payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse, + >, + > { + let mut request = tonic::Request::new(create_access_token_request); + let connector_name = connector_auth_metadata.connector_name.clone(); + let metadata = build_unified_connector_service_grpc_headers_for_payouts( + connector_auth_metadata, + grpc_headers, + )?; + *request.metadata_mut() = metadata; + + self.merchant_authentication_service_client + .clone() + .create_server_authentication_token(request) + .await + .map_err(|error| { + error_stack::Report::new(UnifiedConnectorServiceError::from_grpc_error( + &error, + &connector_name, + )) + }) + .inspect_err(|error| { + logger::error!( + grpc_error=?error, + method="create_server_authentication_token_for_payouts", + connector_name=?connector_name, + "UCS create server authentication token gRPC call failed" + ) + }) + } + /// Performs Payout Transfer pub async fn payout_transfer( &self, diff --git a/crates/router/src/core/payments/gateway/access_token_gateway.rs b/crates/router/src/core/payments/gateway/access_token_gateway.rs index c68af058cdf..a68c07a5069 100644 --- a/crates/router/src/core/payments/gateway/access_token_gateway.rs +++ b/crates/router/src/core/payments/gateway/access_token_gateway.rs @@ -102,20 +102,48 @@ where ) .change_context(ConnectorError::RequestEncodingFailed) .attach_printable("Failed to construct request metadata")?; - let merchant_reference_id = unified_connector_service::parse_merchant_reference_id( - header_payload - .x_reference_id - .as_deref() - .unwrap_or(router_data.payment_id.as_str()), - ) - .map(ucs_types::UcsReferenceId::Payment); - - let resource_id = id_type::PaymentResourceId::from_str(router_data.attempt_id.as_str()) - .inspect_err( - |err| logger::warn!(error=?err, "Invalid Payment AttemptId for UCS resource id"), - ) - .ok() - .map(ucs_types::UcsResourceId::PaymentAttempt); + // A merchant-authentication (access-token) call can originate from either a + // payment or a payout. When it is a payout, attach the payout reference/resource + // ids so the downstream call carries the real payout reference and (via the + // payout gRPC client) resolves the payout connector variant. + let is_payout = router_data.payout_id.is_some(); + + let (merchant_reference_id, resource_id) = + if let Some(payout_id) = router_data.payout_id.as_deref() { + let merchant_reference_id = + unified_connector_service::parse_merchant_payout_reference_id( + header_payload.x_reference_id.as_deref().unwrap_or(payout_id), + ) + .map(ucs_types::UcsReferenceId::Payout); + + let resource_id = + id_type::PayoutResourceId::from_str(router_data.attempt_id.as_str()) + .inspect_err(|err| { + logger::warn!(error=?err, "Invalid Payout AttemptId for UCS resource id") + }) + .ok() + .map(ucs_types::UcsResourceId::PayoutAttempt); + + (merchant_reference_id, resource_id) + } else { + let merchant_reference_id = unified_connector_service::parse_merchant_reference_id( + header_payload + .x_reference_id + .as_deref() + .unwrap_or(router_data.payment_id.as_str()), + ) + .map(ucs_types::UcsReferenceId::Payment); + + let resource_id = + id_type::PaymentResourceId::from_str(router_data.attempt_id.as_str()) + .inspect_err(|err| { + logger::warn!(error=?err, "Invalid Payment AttemptId for UCS resource id") + }) + .ok() + .map(ucs_types::UcsResourceId::PaymentAttempt); + + (merchant_reference_id, resource_id) + }; let header_payload = state .get_grpc_headers_ucs(unified_connector_service_execution_mode) @@ -131,14 +159,24 @@ where header_payload, unified_connector_service_execution_mode, |mut router_data, create_access_token_request, grpc_headers| async move { - let response = client - .create_access_token( - create_access_token_request, - connector_auth_metadata, - grpc_headers, - ) - .await - .attach_printable("Failed to create access token")?; + let response = if is_payout { + client + .create_access_token_for_payouts( + create_access_token_request, + connector_auth_metadata, + grpc_headers, + ) + .await + } else { + client + .create_access_token( + create_access_token_request, + connector_auth_metadata, + grpc_headers, + ) + .await + } + .attach_printable("Failed to create access token")?; let create_access_token_response = response.into_inner(); From 27f6e2840bf12c55303556ff00748f01c2b58905 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 07:14:28 +0000 Subject: [PATCH 2/3] chore: run formatter --- .../payments/gateway/access_token_gateway.rs | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/crates/router/src/core/payments/gateway/access_token_gateway.rs b/crates/router/src/core/payments/gateway/access_token_gateway.rs index a68c07a5069..c428515a461 100644 --- a/crates/router/src/core/payments/gateway/access_token_gateway.rs +++ b/crates/router/src/core/payments/gateway/access_token_gateway.rs @@ -108,33 +108,36 @@ where // payout gRPC client) resolves the payout connector variant. let is_payout = router_data.payout_id.is_some(); - let (merchant_reference_id, resource_id) = - if let Some(payout_id) = router_data.payout_id.as_deref() { - let merchant_reference_id = - unified_connector_service::parse_merchant_payout_reference_id( - header_payload.x_reference_id.as_deref().unwrap_or(payout_id), - ) - .map(ucs_types::UcsReferenceId::Payout); - - let resource_id = - id_type::PayoutResourceId::from_str(router_data.attempt_id.as_str()) - .inspect_err(|err| { - logger::warn!(error=?err, "Invalid Payout AttemptId for UCS resource id") - }) - .ok() - .map(ucs_types::UcsResourceId::PayoutAttempt); - - (merchant_reference_id, resource_id) - } else { - let merchant_reference_id = unified_connector_service::parse_merchant_reference_id( + let (merchant_reference_id, resource_id) = if let Some(payout_id) = + router_data.payout_id.as_deref() + { + let merchant_reference_id = + unified_connector_service::parse_merchant_payout_reference_id( header_payload .x_reference_id .as_deref() - .unwrap_or(router_data.payment_id.as_str()), + .unwrap_or(payout_id), + ) + .map(ucs_types::UcsReferenceId::Payout); + + let resource_id = id_type::PayoutResourceId::from_str(router_data.attempt_id.as_str()) + .inspect_err( + |err| logger::warn!(error=?err, "Invalid Payout AttemptId for UCS resource id"), ) - .map(ucs_types::UcsReferenceId::Payment); + .ok() + .map(ucs_types::UcsResourceId::PayoutAttempt); + + (merchant_reference_id, resource_id) + } else { + let merchant_reference_id = unified_connector_service::parse_merchant_reference_id( + header_payload + .x_reference_id + .as_deref() + .unwrap_or(router_data.payment_id.as_str()), + ) + .map(ucs_types::UcsReferenceId::Payment); - let resource_id = + let resource_id = id_type::PaymentResourceId::from_str(router_data.attempt_id.as_str()) .inspect_err(|err| { logger::warn!(error=?err, "Invalid Payment AttemptId for UCS resource id") @@ -142,8 +145,8 @@ where .ok() .map(ucs_types::UcsResourceId::PaymentAttempt); - (merchant_reference_id, resource_id) - }; + (merchant_reference_id, resource_id) + }; let header_payload = state .get_grpc_headers_ucs(unified_connector_service_execution_mode) From d417197edc07fc3c87284468d61cb4930ceda7f9 Mon Sep 17 00:00:00 2001 From: Gopikrishna C Date: Fri, 3 Jul 2026 18:36:52 +0530 Subject: [PATCH 3/3] refactor(payouts): parameterize create_access_token with is_payout instead of a payout sibling Address review: collapse create_access_token_for_payouts into create_access_token via an is_payout flag that selects the payout vs payment gRPC header builder, removing the duplicated function body. --- .../grpc_client/unified_connector_service.rs | 58 +++++-------------- .../router/src/core/payments/access_token.rs | 2 +- .../payments/gateway/access_token_gateway.rs | 27 +++------ 3 files changed, 23 insertions(+), 64 deletions(-) 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 7cd3ef1cb4b..cd662376738 100644 --- a/crates/external_services/src/grpc_client/unified_connector_service.rs +++ b/crates/external_services/src/grpc_client/unified_connector_service.rs @@ -1047,11 +1047,16 @@ impl UnifiedConnectorServiceClient { } /// Performs Create Access Token Granular + /// + /// When `is_payout` is set, the payout connector header (`x-payout-connector`) is + /// attached so the merchant-authentication flow resolves the payout connector + /// variant; otherwise the payment connector header (`x-connector`) is used. pub async fn create_access_token( &self, create_access_token_request: payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest, connector_auth_metadata: ConnectorAuthMetadata, grpc_headers: GrpcHeadersUcs, + is_payout: bool, ) -> UnifiedConnectorServiceResult< tonic::Response< payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse, @@ -1059,8 +1064,14 @@ impl UnifiedConnectorServiceClient { > { let mut request = tonic::Request::new(create_access_token_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 = if is_payout { + build_unified_connector_service_grpc_headers_for_payouts( + connector_auth_metadata, + grpc_headers, + )? + } else { + build_unified_connector_service_grpc_headers(connector_auth_metadata, grpc_headers)? + }; *request.metadata_mut() = metadata; self.merchant_authentication_service_client @@ -1083,49 +1094,6 @@ impl UnifiedConnectorServiceClient { }) } - /// Performs Create Access Token Granular for payouts - /// - /// Identical to [`Self::create_access_token`] but attaches the payout connector - /// header (`x-payout-connector`) so the merchant-authentication flow resolves the - /// payout connector variant instead of the payment one. - pub async fn create_access_token_for_payouts( - &self, - create_access_token_request: payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest, - connector_auth_metadata: ConnectorAuthMetadata, - grpc_headers: GrpcHeadersUcs, - ) -> UnifiedConnectorServiceResult< - tonic::Response< - payments_grpc::MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse, - >, - > { - let mut request = tonic::Request::new(create_access_token_request); - let connector_name = connector_auth_metadata.connector_name.clone(); - let metadata = build_unified_connector_service_grpc_headers_for_payouts( - connector_auth_metadata, - grpc_headers, - )?; - *request.metadata_mut() = metadata; - - self.merchant_authentication_service_client - .clone() - .create_server_authentication_token(request) - .await - .map_err(|error| { - error_stack::Report::new(UnifiedConnectorServiceError::from_grpc_error( - &error, - &connector_name, - )) - }) - .inspect_err(|error| { - logger::error!( - grpc_error=?error, - method="create_server_authentication_token_for_payouts", - connector_name=?connector_name, - "UCS create server authentication token gRPC call failed" - ) - }) - } - /// Performs Payout Transfer pub async fn payout_transfer( &self, diff --git a/crates/router/src/core/payments/access_token.rs b/crates/router/src/core/payments/access_token.rs index 07c1a67ce7e..ab9656b2f3e 100644 --- a/crates/router/src/core/payments/access_token.rs +++ b/crates/router/src/core/payments/access_token.rs @@ -406,7 +406,7 @@ async fn fetch_access_token_from_ucs( }; let response = client - .create_access_token(create_request, connector_auth_metadata, grpc_headers) + .create_access_token(create_request, connector_auth_metadata, grpc_headers, false) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("UCS create_access_token gRPC call failed")?; diff --git a/crates/router/src/core/payments/gateway/access_token_gateway.rs b/crates/router/src/core/payments/gateway/access_token_gateway.rs index c428515a461..7013c1d77f8 100644 --- a/crates/router/src/core/payments/gateway/access_token_gateway.rs +++ b/crates/router/src/core/payments/gateway/access_token_gateway.rs @@ -162,24 +162,15 @@ where header_payload, unified_connector_service_execution_mode, |mut router_data, create_access_token_request, grpc_headers| async move { - let response = if is_payout { - client - .create_access_token_for_payouts( - create_access_token_request, - connector_auth_metadata, - grpc_headers, - ) - .await - } else { - client - .create_access_token( - create_access_token_request, - connector_auth_metadata, - grpc_headers, - ) - .await - } - .attach_printable("Failed to create access token")?; + let response = client + .create_access_token( + create_access_token_request, + connector_auth_metadata, + grpc_headers, + is_payout, + ) + .await + .attach_printable("Failed to create access token")?; let create_access_token_response = response.into_inner();