feat(core): Implement UCS based upi for paytm and phonepe (#8732)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com>
This commit is contained in:
Uzair Khan
2025-07-31 19:17:04 +05:30
committed by GitHub
parent c6e4e7209f
commit 01e9474808
39 changed files with 3098 additions and 51 deletions

View File

@ -32,18 +32,18 @@ pub use hyperswitch_connectors::connectors::{
novalnet::Novalnet, nuvei, nuvei::Nuvei, opayo, opayo::Opayo, opennode, opennode::Opennode,
paybox, paybox::Paybox, payeezy, payeezy::Payeezy, payload, payload::Payload, payme,
payme::Payme, payone, payone::Payone, paypal, paypal::Paypal, paystack, paystack::Paystack,
payu, payu::Payu, placetopay, placetopay::Placetopay, plaid, plaid::Plaid, powertranz,
powertranz::Powertranz, prophetpay, prophetpay::Prophetpay, rapyd, rapyd::Rapyd, razorpay,
razorpay::Razorpay, recurly, recurly::Recurly, redsys, redsys::Redsys, riskified,
riskified::Riskified, santander, santander::Santander, shift4, shift4::Shift4, signifyd,
signifyd::Signifyd, silverflow, silverflow::Silverflow, square, square::Square, stax,
stax::Stax, stripe, stripe::Stripe, stripebilling, stripebilling::Stripebilling, taxjar,
taxjar::Taxjar, threedsecureio, threedsecureio::Threedsecureio, thunes, thunes::Thunes,
tokenio, tokenio::Tokenio, trustpay, trustpay::Trustpay, trustpayments,
trustpayments::Trustpayments, tsys, tsys::Tsys, unified_authentication_service,
unified_authentication_service::UnifiedAuthenticationService, vgs, vgs::Vgs, volt, volt::Volt,
wellsfargo, wellsfargo::Wellsfargo, wellsfargopayout, wellsfargopayout::Wellsfargopayout, wise,
wise::Wise, worldline, worldline::Worldline, worldpay, worldpay::Worldpay, worldpayvantiv,
worldpayvantiv::Worldpayvantiv, worldpayxml, worldpayxml::Worldpayxml, xendit, xendit::Xendit,
zen, zen::Zen, zsl, zsl::Zsl,
paytm, paytm::Paytm, payu, payu::Payu, phonepe, phonepe::Phonepe, placetopay,
placetopay::Placetopay, plaid, plaid::Plaid, powertranz, powertranz::Powertranz, prophetpay,
prophetpay::Prophetpay, rapyd, rapyd::Rapyd, razorpay, razorpay::Razorpay, recurly,
recurly::Recurly, redsys, redsys::Redsys, riskified, riskified::Riskified, santander,
santander::Santander, shift4, shift4::Shift4, signifyd, signifyd::Signifyd, silverflow,
silverflow::Silverflow, square, square::Square, stax, stax::Stax, stripe, stripe::Stripe,
stripebilling, stripebilling::Stripebilling, taxjar, taxjar::Taxjar, threedsecureio,
threedsecureio::Threedsecureio, thunes, thunes::Thunes, tokenio, tokenio::Tokenio, trustpay,
trustpay::Trustpay, trustpayments, trustpayments::Trustpayments, tsys, tsys::Tsys,
unified_authentication_service, unified_authentication_service::UnifiedAuthenticationService,
vgs, vgs::Vgs, volt, volt::Volt, wellsfargo, wellsfargo::Wellsfargo, wellsfargopayout,
wellsfargopayout::Wellsfargopayout, wise, wise::Wise, worldline, worldline::Worldline,
worldpay, worldpay::Worldpay, worldpayvantiv, worldpayvantiv::Worldpayvantiv, worldpayxml,
worldpayxml::Worldpayxml, xendit, xendit::Xendit, zen, zen::Zen, zsl, zsl::Zsl,
};

View File

@ -522,6 +522,14 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> {
threedsecureio::transformers::ThreedsecureioAuthType::try_from(self.auth_type)?;
Ok(())
}
api_enums::Connector::Phonepe => {
phonepe::transformers::PhonepeAuthType::try_from(self.auth_type)?;
Ok(())
}
api_enums::Connector::Paytm => {
paytm::transformers::PaytmAuthType::try_from(self.auth_type)?;
Ok(())
}
}
}
}

View File

@ -57,6 +57,16 @@ pub async fn should_call_unified_connector_service<F: Clone, T>(
let payment_method = router_data.payment_method.to_string();
let flow_name = get_flow_name::<F>()?;
let is_ucs_only_connector = state
.conf
.grpc_client
.unified_connector_service
.as_ref()
.is_some_and(|config| config.ucs_only_connectors.contains(&connector_name));
if is_ucs_only_connector {
return Ok(true);
}
let config_key = format!(
"{}_{}_{}_{}_{}",
consts::UCS_ROLLOUT_PERCENT_CONFIG_PREFIX,
@ -135,11 +145,9 @@ pub fn build_unified_connector_service_payment_method(
let upi_details = payments_grpc::UpiCollect { vpa_id };
PaymentMethod::UpiCollect(upi_details)
}
_ => {
return Err(UnifiedConnectorServiceError::NotImplemented(format!(
"Unimplemented payment method subtype: {payment_method_type:?}"
))
.into());
hyperswitch_domain_models::payment_method_data::UpiData::UpiIntent(_) => {
let upi_details = payments_grpc::UpiIntent {};
PaymentMethod::UpiIntent(upi_details)
}
};
@ -238,6 +246,112 @@ pub fn handle_unified_connector_service_response_for_payment_authorize(
> {
let status = AttemptStatus::foreign_try_from(response.status())?;
// <<<<<<< HEAD
// let connector_response_reference_id =
// response.response_ref_id.as_ref().and_then(|identifier| {
// identifier
// .id_type
// .clone()
// .and_then(|id_type| match id_type {
// payments_grpc::identifier::IdType::Id(id) => Some(id),
// payments_grpc::identifier::IdType::EncodedData(encoded_data) => {
// Some(encoded_data)
// }
// payments_grpc::identifier::IdType::NoResponseIdMarker(_) => None,
// })
// });
// let transaction_id = response.transaction_id.as_ref().and_then(|id| {
// id.id_type.clone().and_then(|id_type| match id_type {
// payments_grpc::identifier::IdType::Id(id) => Some(id),
// payments_grpc::identifier::IdType::EncodedData(encoded_data) => Some(encoded_data),
// payments_grpc::identifier::IdType::NoResponseIdMarker(_) => None,
// })
// });
// let (connector_metadata, redirection_data) = match response.redirection_data.clone() {
// Some(redirection_data) => match redirection_data.form_type {
// Some(ref form_type) => match form_type {
// payments_grpc::redirect_form::FormType::Uri(uri) => {
// let image_data = QrImage::new_from_data(uri.uri.clone())
// .change_context(UnifiedConnectorServiceError::ParsingFailed)?;
// let image_data_url = Url::parse(image_data.data.clone().as_str())
// .change_context(UnifiedConnectorServiceError::ParsingFailed)?;
// let qr_code_info = QrCodeInformation::QrDataUrl {
// image_data_url,
// display_to_timestamp: None,
// };
// (
// Some(qr_code_info.encode_to_value())
// .transpose()
// .change_context(UnifiedConnectorServiceError::ParsingFailed)?,
// None,
// )
// }
// _ => (
// None,
// Some(RedirectForm::foreign_try_from(redirection_data)).transpose()?,
// ),
// },
// None => (None, None),
// },
// None => (None, None),
// };
// let router_data_response = match status {
// AttemptStatus::Charged |
// AttemptStatus::Authorized |
// AttemptStatus::AuthenticationPending |
// AttemptStatus::DeviceDataCollectionPending |
// AttemptStatus::Started |
// AttemptStatus::AuthenticationSuccessful |
// AttemptStatus::Authorizing |
// AttemptStatus::ConfirmationAwaited |
// AttemptStatus::Pending => Ok(PaymentsResponseData::TransactionResponse {
// resource_id: match transaction_id.as_ref() {
// Some(transaction_id) => hyperswitch_domain_models::router_request_types::ResponseId::ConnectorTransactionId(transaction_id.clone()),
// None => hyperswitch_domain_models::router_request_types::ResponseId::NoResponseId,
// },
// redirection_data: Box::new(
// redirection_data
// ),
// mandate_reference: Box::new(None),
// connector_metadata,
// network_txn_id: response.network_txn_id.clone(),
// connector_response_reference_id,
// incremental_authorization_allowed: response.incremental_authorization_allowed,
// charges: None,
// }),
// AttemptStatus::AuthenticationFailed
// | AttemptStatus::AuthorizationFailed
// | AttemptStatus::Unresolved
// | AttemptStatus::Failure => Err(ErrorResponse {
// code: response.error_code().to_owned(),
// message: response.error_message().to_owned(),
// reason: Some(response.error_message().to_owned()),
// status_code: 500,
// attempt_status: Some(status),
// connector_transaction_id: connector_response_reference_id,
// network_decline_code: None,
// network_advice_code: None,
// network_error_message: None,
// }),
// AttemptStatus::RouterDeclined |
// AttemptStatus::CodInitiated |
// AttemptStatus::Voided |
// AttemptStatus::VoidInitiated |
// AttemptStatus::CaptureInitiated |
// AttemptStatus::VoidFailed |
// AttemptStatus::AutoRefunded |
// AttemptStatus::PartialCharged |
// AttemptStatus::PartialChargedAndChargeable |
// AttemptStatus::PaymentMethodAwaited |
// AttemptStatus::CaptureFailed |
// AttemptStatus::IntegrityFailure => return Err(UnifiedConnectorServiceError::NotImplemented(format!(
// "AttemptStatus {status:?} is not implemented for Unified Connector Service"
// )).into()),
// };
// =======
let router_data_response =
Result::<PaymentsResponseData, ErrorResponse>::foreign_try_from(response)?;

View File

@ -1,10 +1,12 @@
use std::collections::HashMap;
use api_models::payments::QrCodeInformation;
use common_enums::{AttemptStatus, AuthenticationType};
use common_utils::request::Method;
use common_utils::{ext_traits::Encode, request::Method};
use diesel_models::enums as storage_enums;
use error_stack::ResultExt;
use external_services::grpc_client::unified_connector_service::UnifiedConnectorServiceError;
use hyperswitch_connectors::utils::QrImage;
use hyperswitch_domain_models::{
router_data::{ErrorResponse, RouterData},
router_flow_types::payments::{Authorize, PSync, SetupMandate},
@ -14,7 +16,9 @@ use hyperswitch_domain_models::{
router_response_types::{PaymentsResponseData, RedirectForm},
};
use masking::{ExposeInterface, PeekInterface};
use router_env::tracing;
use unified_connector_service_client::payments::{self as payments_grpc, Identifier};
use url::Url;
use crate::{
core::unified_connector_service::build_unified_connector_service_payment_method,
@ -364,6 +368,35 @@ impl ForeignTryFrom<payments_grpc::PaymentServiceAuthorizeResponse>
})
});
let (connector_metadata, redirection_data) = match response.redirection_data.clone() {
Some(redirection_data) => match redirection_data.form_type {
Some(ref form_type) => match form_type {
payments_grpc::redirect_form::FormType::Uri(uri) => {
let image_data = QrImage::new_from_data(uri.uri.clone())
.change_context(UnifiedConnectorServiceError::ParsingFailed)?;
let image_data_url = Url::parse(image_data.data.clone().as_str())
.change_context(UnifiedConnectorServiceError::ParsingFailed)?;
let qr_code_info = QrCodeInformation::QrDataUrl {
image_data_url,
display_to_timestamp: None,
};
(
Some(qr_code_info.encode_to_value())
.transpose()
.change_context(UnifiedConnectorServiceError::ParsingFailed)?,
None,
)
}
_ => (
None,
Some(RedirectForm::foreign_try_from(redirection_data)).transpose()?,
),
},
None => (None, None),
},
None => (None, None),
};
let response = if response.error_code.is_some() {
Err(ErrorResponse {
code: response.error_code().to_owned(),
@ -383,14 +416,10 @@ impl ForeignTryFrom<payments_grpc::PaymentServiceAuthorizeResponse>
None => hyperswitch_domain_models::router_request_types::ResponseId::NoResponseId,
},
redirection_data: Box::new(
response
.redirection_data
.clone()
.map(RedirectForm::foreign_try_from)
.transpose()?
redirection_data
),
mandate_reference: Box::new(None),
connector_metadata: None,
connector_metadata,
network_txn_id: response.network_txn_id.clone(),
connector_response_reference_id,
incremental_authorization_allowed: response.incremental_authorization_allowed,
@ -907,6 +936,7 @@ impl ForeignTryFrom<payments_grpc::HttpMethod> for Method {
type Error = error_stack::Report<UnifiedConnectorServiceError>;
fn foreign_try_from(value: payments_grpc::HttpMethod) -> Result<Self, Self::Error> {
tracing::debug!("Converting gRPC HttpMethod: {:?}", value);
match value {
payments_grpc::HttpMethod::Get => Ok(Self::Get),
payments_grpc::HttpMethod::Post => Ok(Self::Post),

View File

@ -1,6 +1,7 @@
use std::str::FromStr;
use error_stack::{report, ResultExt};
use hyperswitch_connectors::connectors::{Paytm, Phonepe};
use crate::{
configs::settings::Connectors,
@ -442,6 +443,8 @@ impl ConnectorData {
.attach_printable(format!("invalid connector name: {connector_name}")))
.change_context(errors::ApiErrorResponse::InternalServerError)
}
enums::Connector::Phonepe => Ok(ConnectorEnum::Old(Box::new(Phonepe::new()))),
enums::Connector::Paytm => Ok(ConnectorEnum::Old(Box::new(Paytm::new()))),
},
Err(_) => Err(report!(errors::ConnectorError::InvalidConnectorName)
.attach_printable(format!("invalid connector name: {connector_name}")))

View File

@ -187,6 +187,8 @@ impl ForeignTryFrom<api_enums::Connector> for common_enums::RoutableConnectors {
message: "Taxjar is not a routable connector".to_string(),
})?
}
api_enums::Connector::Phonepe => Self::Phonepe,
api_enums::Connector::Paytm => Self::Paytm,
})
}
}