fix(connector): [Tokenex] fix tokenize flow response handling for tokenex (#9528)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Prasunna Soppa
2025-09-25 19:16:51 +05:30
committed by GitHub
parent 30909beae0
commit 84f3013c88
7 changed files with 213 additions and 75 deletions

View File

@ -151,11 +151,13 @@ impl ConnectorCommon for Tokenex {
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
let (code, message) = response.error.split_once(':').unwrap_or(("", ""));
Ok(ErrorResponse {
status_code: res.status_code,
code: response.code,
message: response.message,
reason: response.reason,
code: code.to_string(),
message: message.to_string(),
reason: Some(response.message),
attempt_status: None,
connector_transaction_id: None,
network_advice_code: None,

View File

@ -1,10 +1,7 @@
use common_utils::{
ext_traits::{Encode, StringExt},
types::StringMinorUnit,
};
use common_utils::types::StringMinorUnit;
use error_stack::ResultExt;
use hyperswitch_domain_models::{
router_data::{ConnectorAuthType, RouterData},
router_data::{ConnectorAuthType, ErrorResponse, RouterData},
router_flow_types::{ExternalVaultInsertFlow, ExternalVaultRetrieveFlow},
router_request_types::VaultRequestData,
router_response_types::VaultResponseData,
@ -12,7 +9,7 @@ use hyperswitch_domain_models::{
vault::PaymentMethodVaultingData,
};
use hyperswitch_interfaces::errors;
use masking::{ExposeInterface, Secret};
use masking::Secret;
use serde::{Deserialize, Serialize};
use crate::types::ResponseRouterData;
@ -24,7 +21,6 @@ pub struct TokenexRouterData<T> {
impl<T> From<(StringMinorUnit, T)> for TokenexRouterData<T> {
fn from((amount, item): (StringMinorUnit, T)) -> Self {
//Todo : use utils to convert the amount to the type of amount that a connector accepts
Self {
amount,
router_data: item,
@ -34,21 +30,16 @@ impl<T> From<(StringMinorUnit, T)> for TokenexRouterData<T> {
#[derive(Default, Debug, Serialize, PartialEq)]
pub struct TokenexInsertRequest {
data: Secret<String>,
data: cards::CardNumber, //Currently only card number is tokenized. Data can be stringified and can be tokenized
}
impl<F> TryFrom<&VaultRouterData<F>> for TokenexInsertRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &VaultRouterData<F>) -> Result<Self, Self::Error> {
match item.request.payment_method_vaulting_data.clone() {
Some(PaymentMethodVaultingData::Card(req_card)) => {
let stringified_card = req_card
.encode_to_string_of_json()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Self {
data: Secret::new(stringified_card),
})
}
Some(PaymentMethodVaultingData::Card(req_card)) => Ok(Self {
data: req_card.card_number.clone(),
}),
_ => Err(errors::ConnectorError::NotImplemented(
"Payment method apart from card".to_string(),
)
@ -56,9 +47,6 @@ impl<F> TryFrom<&VaultRouterData<F>> for TokenexInsertRequest {
}
}
}
//TODO: Fill the struct with respective fields
// Auth Struct
pub struct TokenexAuthType {
pub(super) api_key: Secret<String>,
pub(super) tokenex_id: Secret<String>,
@ -78,10 +66,14 @@ impl TryFrom<&ConnectorAuthType> for TokenexAuthType {
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TokenexInsertResponse {
token: String,
first_six: String,
token: Option<String>,
first_six: Option<String>,
last_four: Option<String>,
success: bool,
error: String,
message: Option<String>,
}
impl
TryFrom<
@ -103,20 +95,49 @@ impl
>,
) -> Result<Self, Self::Error> {
let resp = item.response;
match resp.success && resp.error.is_empty() {
true => {
let token = resp
.token
.clone()
.ok_or(errors::ConnectorError::ResponseDeserializationFailed)
.attach_printable("Token is missing in tokenex response")?;
Ok(Self {
status: common_enums::AttemptStatus::Started,
response: Ok(VaultResponseData::ExternalVaultInsertResponse {
connector_vault_id: token.clone(),
//fingerprint is not provided by tokenex, using token as fingerprint
fingerprint_id: token.clone(),
}),
..item.data
})
}
false => {
let (code, message) = resp.error.split_once(':').unwrap_or(("", ""));
Ok(Self {
status: common_enums::AttemptStatus::Started,
response: Ok(VaultResponseData::ExternalVaultInsertResponse {
connector_vault_id: resp.token.clone(),
//fingerprint is not provided by tokenex, using token as fingerprint
fingerprint_id: resp.token.clone(),
}),
..item.data
})
let response = Err(ErrorResponse {
code: code.to_string(),
message: message.to_string(),
reason: resp.message,
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: None,
network_decline_code: None,
network_advice_code: None,
network_error_message: None,
connector_metadata: None,
});
Ok(Self {
response,
..item.data
})
}
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TokenexRetrieveRequest {
token: Secret<String>, //Currently only card number is tokenized. Data can be stringified and can be tokenized
cache_cvv: bool,
@ -124,8 +145,10 @@ pub struct TokenexRetrieveRequest {
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TokenexRetrieveResponse {
value: Secret<String>,
value: Option<cards::CardNumber>,
success: bool,
error: String,
message: Option<String>,
}
impl<F> TryFrom<&VaultRouterData<F>> for TokenexRetrieveRequest {
@ -164,30 +187,48 @@ impl
) -> Result<Self, Self::Error> {
let resp = item.response;
let card_detail: api_models::payment_methods::CardDetail = resp
.value
.clone()
.expose()
.parse_struct("CardDetail")
.change_context(errors::ConnectorError::ParsingFailed)?;
match resp.success && resp.error.is_empty() {
true => {
let data = resp
.value
.clone()
.ok_or(errors::ConnectorError::ResponseDeserializationFailed)
.attach_printable("Card number is missing in tokenex response")?;
Ok(Self {
status: common_enums::AttemptStatus::Started,
response: Ok(VaultResponseData::ExternalVaultRetrieveResponse {
vault_data: PaymentMethodVaultingData::CardNumber(data),
}),
..item.data
})
}
false => {
let (code, message) = resp.error.split_once(':').unwrap_or(("", ""));
Ok(Self {
status: common_enums::AttemptStatus::Started,
response: Ok(VaultResponseData::ExternalVaultRetrieveResponse {
vault_data: PaymentMethodVaultingData::Card(card_detail),
}),
..item.data
})
let response = Err(ErrorResponse {
code: code.to_string(),
message: message.to_string(),
reason: resp.message,
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: None,
network_decline_code: None,
network_advice_code: None,
network_error_message: None,
connector_metadata: None,
});
Ok(Self {
response,
..item.data
})
}
}
}
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct TokenexErrorResponse {
pub status_code: u16,
pub code: String,
pub error: String,
pub message: String,
pub reason: Option<String>,
pub network_advice_code: Option<String>,
pub network_decline_code: Option<String>,
pub network_error_message: Option<String>,
}

View File

@ -7,10 +7,8 @@ use api_models::{
payments::{additional_info as payment_additional_types, ExtendedCardInfo},
};
use common_enums::enums as api_enums;
#[cfg(feature = "v2")]
use common_utils::ext_traits::OptionExt;
use common_utils::{
ext_traits::StringExt,
ext_traits::{OptionExt, StringExt},
id_type,
new_type::{
MaskedBankAccount, MaskedIban, MaskedRoutingNumber, MaskedSortCode, MaskedUpiVpaId,
@ -18,7 +16,7 @@ use common_utils::{
payout_method_utils,
pii::{self, Email},
};
use masking::{PeekInterface, Secret};
use masking::{ExposeInterface, PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use time::Date;
@ -2327,6 +2325,13 @@ impl PaymentMethodsData {
Self::BankDetails(_) | Self::WalletDetails(_) | Self::NetworkToken(_) => None,
}
}
pub fn get_card_details(&self) -> Option<CardDetailsPaymentMethod> {
if let Self::Card(card) = self {
Some(card.clone())
} else {
None
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@ -2593,8 +2598,6 @@ impl
// The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked
let name_on_card = if let Some(name) = card_holder_name.clone() {
use masking::ExposeInterface;
if name.clone().expose().is_empty() {
card_token_data
.and_then(|token_data| token_data.card_holder_name.clone())
@ -2626,3 +2629,63 @@ impl
}
}
}
#[cfg(feature = "v1")]
impl
TryFrom<(
cards::CardNumber,
Option<&CardToken>,
Option<payment_methods::CoBadgedCardData>,
CardDetailsPaymentMethod,
)> for Card
{
type Error = error_stack::Report<common_utils::errors::ValidationError>;
fn try_from(
value: (
cards::CardNumber,
Option<&CardToken>,
Option<payment_methods::CoBadgedCardData>,
CardDetailsPaymentMethod,
),
) -> Result<Self, Self::Error> {
let (card_number, card_token_data, co_badged_card_data, card_details) = value;
// The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked
let name_on_card = if let Some(name) = card_details.card_holder_name.clone() {
if name.clone().expose().is_empty() {
card_token_data
.and_then(|token_data| token_data.card_holder_name.clone())
.or(Some(name))
} else {
Some(name)
}
} else {
card_token_data.and_then(|token_data| token_data.card_holder_name.clone())
};
Ok(Self {
card_number,
card_exp_month: card_details
.expiry_month
.get_required_value("expiry_month")?
.clone(),
card_exp_year: card_details
.expiry_year
.get_required_value("expiry_year")?
.clone(),
card_holder_name: name_on_card,
card_cvc: card_token_data
.cloned()
.unwrap_or_default()
.card_cvc
.unwrap_or_default(),
card_issuer: card_details.card_issuer,
card_network: card_details.card_network,
card_type: card_details.card_type,
card_issuing_country: card_details.issuer_country,
bank_code: None,
nick_name: card_details.nick_name,
co_badged_card_data,
})
}
}

View File

@ -12,6 +12,7 @@ pub enum PaymentMethodVaultingData {
Card(payment_methods::CardDetail),
#[cfg(feature = "v2")]
NetworkToken(payment_method_data::NetworkTokenDetails),
CardNumber(cards::CardNumber),
}
impl PaymentMethodVaultingData {
@ -20,6 +21,7 @@ impl PaymentMethodVaultingData {
Self::Card(card) => Some(card),
#[cfg(feature = "v2")]
Self::NetworkToken(_) => None,
Self::CardNumber(_) => None,
}
}
pub fn get_payment_methods_data(&self) -> payment_method_data::PaymentMethodsData {
@ -35,6 +37,23 @@ impl PaymentMethodVaultingData {
),
)
}
Self::CardNumber(_card_number) => payment_method_data::PaymentMethodsData::Card(
payment_method_data::CardDetailsPaymentMethod {
last4_digits: None,
issuer_country: None,
expiry_month: None,
expiry_year: None,
nick_name: None,
card_holder_name: None,
card_isin: None,
card_issuer: None,
card_network: None,
card_type: None,
saved_to_locker: false,
#[cfg(feature = "v1")]
co_badged_card_data: None,
},
),
}
}
}
@ -49,6 +68,7 @@ impl VaultingDataInterface for PaymentMethodVaultingData {
Self::Card(card) => card.card_number.to_string(),
#[cfg(feature = "v2")]
Self::NetworkToken(network_token) => network_token.network_token.to_string(),
Self::CardNumber(card_number) => card_number.to_string(),
}
}
}

View File

@ -2201,18 +2201,7 @@ pub async fn create_pm_additional_data_update(
external_vault_source: Option<id_type::MerchantConnectorAccountId>,
) -> RouterResult<storage::PaymentMethodUpdate> {
let encrypted_payment_method_data = pmd
.map(
|payment_method_vaulting_data| match payment_method_vaulting_data {
domain::PaymentMethodVaultingData::Card(card) => domain::PaymentMethodsData::Card(
domain::CardDetailsPaymentMethod::from(card.clone()),
),
domain::PaymentMethodVaultingData::NetworkToken(network_token) => {
domain::PaymentMethodsData::NetworkToken(
domain::NetworkTokenDetailsPaymentMethod::from(network_token.clone()),
)
}
},
)
.map(|payment_method_vaulting_data| payment_method_vaulting_data.get_payment_methods_data())
.async_map(|payment_method_details| async {
let key_manager_state = &(state).into();

View File

@ -1964,9 +1964,12 @@ pub fn get_vault_response_for_retrieve_payment_method_data_v1<F>(
}
},
Err(err) => {
logger::error!("Failed to retrieve payment method: {:?}", err);
logger::error!(
"Failed to retrieve payment method from external vault: {:?}",
err
);
Err(report!(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to retrieve payment method"))
.attach_printable("Failed to retrieve payment method from external vault"))
}
}
}

View File

@ -2512,10 +2512,30 @@ pub async fn fetch_card_details_from_external_vault(
)
.await?;
let payment_methods_data = payment_method_info.get_payment_methods_data();
match vault_resp {
hyperswitch_domain_models::vault::PaymentMethodVaultingData::Card(card) => Ok(
domain::Card::from((card, card_token_data, co_badged_card_data)),
),
hyperswitch_domain_models::vault::PaymentMethodVaultingData::CardNumber(card_number) => {
let payment_methods_data = payment_methods_data
.get_required_value("PaymentMethodsData")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Payment methods data not present")?;
let card = payment_methods_data
.get_card_details()
.get_required_value("CardDetails")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Card details not present")?;
Ok(
domain::Card::try_from((card_number, card_token_data, co_badged_card_data, card))
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to generate card data")?,
)
}
}
}
#[cfg(feature = "v1")]