mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 21:07:58 +08:00
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:
@ -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,
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user