fix(router): associate parent payment token with payment_method_id as hyperswitch token for saved cards (#2130)

Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com>
This commit is contained in:
Shanks
2023-11-20 16:12:06 +05:30
committed by GitHub
parent 644709d95f
commit efeebc0f23
6 changed files with 320 additions and 102 deletions

View File

@ -19,7 +19,7 @@ use storage_impl::errors as storage_impl_errors;
pub use user::*;
pub use self::{
api_error_response::ApiErrorResponse,
api_error_response::{ApiErrorResponse, NotImplementedMessage},
customers_error_response::CustomersErrorResponse,
sch_errors::*,
storage_errors::*,

View File

@ -9,13 +9,17 @@ pub use api_models::{
pub use common_utils::request::RequestBody;
use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent};
use diesel_models::enums;
use error_stack::IntoReport;
use crate::{
core::{errors::RouterResult, payments::helpers},
core::{
errors::{self, RouterResult},
payments::helpers,
},
routes::AppState,
types::{
api::{self, payments},
domain,
domain, storage,
},
};
@ -30,6 +34,14 @@ pub trait PaymentMethodRetrieve {
payment_attempt: &PaymentAttempt,
merchant_key_store: &domain::MerchantKeyStore,
) -> RouterResult<(Option<payments::PaymentMethodData>, Option<String>)>;
async fn retrieve_payment_method_with_token(
state: &AppState,
key_store: &domain::MerchantKeyStore,
token: &storage::PaymentTokenData,
payment_intent: &PaymentIntent,
card_cvc: Option<masking::Secret<String>>,
) -> RouterResult<Option<(payments::PaymentMethodData, enums::PaymentMethod)>>;
}
#[async_trait::async_trait]
@ -105,4 +117,65 @@ impl PaymentMethodRetrieve for Oss {
_ => Ok((None, None)),
}
}
async fn retrieve_payment_method_with_token(
state: &AppState,
merchant_key_store: &domain::MerchantKeyStore,
token_data: &storage::PaymentTokenData,
payment_intent: &PaymentIntent,
card_cvc: Option<masking::Secret<String>>,
) -> RouterResult<Option<(payments::PaymentMethodData, enums::PaymentMethod)>> {
match token_data {
storage::PaymentTokenData::TemporaryGeneric(generic_token) => {
helpers::retrieve_payment_method_with_temporary_token(
state,
&generic_token.token,
payment_intent,
card_cvc,
merchant_key_store,
)
.await
}
storage::PaymentTokenData::Temporary(generic_token) => {
helpers::retrieve_payment_method_with_temporary_token(
state,
&generic_token.token,
payment_intent,
card_cvc,
merchant_key_store,
)
.await
}
storage::PaymentTokenData::Permanent(card_token) => {
helpers::retrieve_card_with_permanent_token(
state,
&card_token.token,
payment_intent,
card_cvc,
)
.await
.map(|card| Some((card, enums::PaymentMethod::Card)))
}
storage::PaymentTokenData::PermanentCard(card_token) => {
helpers::retrieve_card_with_permanent_token(
state,
&card_token.token,
payment_intent,
card_cvc,
)
.await
.map(|card| Some((card, enums::PaymentMethod::Card)))
}
storage::PaymentTokenData::AuthBankDebit(_) => {
Err(errors::ApiErrorResponse::NotImplemented {
message: errors::NotImplementedMessage::Default,
})
.into_report()
}
}
}
}

View File

@ -50,7 +50,7 @@ use crate::{
self,
types::{decrypt, encrypt_optional, AsyncLift},
},
storage::{self, enums},
storage::{self, enums, PaymentTokenData},
transformers::ForeignFrom,
},
utils::{self, ConnectorResponseExt, OptionExt},
@ -2103,23 +2103,32 @@ pub async fn list_customer_payment_method(
let mut customer_pms = Vec::new();
for pm in resp.into_iter() {
let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token");
let hyperswitch_token = generate_id(consts::ID_LENGTH, "token");
let card = if pm.payment_method == enums::PaymentMethod::Card {
get_card_details(&pm, key, state, &hyperswitch_token, &key_store).await?
} else {
None
let (card, pmd, hyperswitch_token_data) = match pm.payment_method {
enums::PaymentMethod::Card => (
Some(get_card_details(&pm, key, state).await?),
None,
PaymentTokenData::permanent_card(pm.payment_method_id.clone()),
),
#[cfg(feature = "payouts")]
enums::PaymentMethod::BankTransfer => {
let token = generate_id(consts::ID_LENGTH, "token");
let token_data = PaymentTokenData::temporary_generic(token.clone());
(
None,
Some(get_lookup_key_for_payout_method(state, &key_store, &token, &pm).await?),
token_data,
)
}
_ => (
None,
None,
PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")),
),
};
#[cfg(feature = "payouts")]
let pmd = if pm.payment_method == enums::PaymentMethod::BankTransfer {
Some(
get_lookup_key_for_payout_method(state, &key_store, &hyperswitch_token, &pm)
.await?,
)
} else {
None
};
//Need validation for enabled payment method ,querying MCA
let pma = api::CustomerPaymentMethod {
payment_token: parent_payment_method_token.to_owned(),
@ -2134,10 +2143,7 @@ pub async fn list_customer_payment_method(
installment_payment_enabled: false,
payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]),
created: Some(pm.created_at),
#[cfg(feature = "payouts")]
bank_transfer: pmd,
#[cfg(not(feature = "payouts"))]
bank_transfer: None,
requires_cvv,
};
customer_pms.push(pma.to_owned());
@ -2153,7 +2159,7 @@ pub async fn list_customer_payment_method(
&parent_payment_method_token,
pma.payment_method,
))
.insert(intent_created, hyperswitch_token, state)
.insert(intent_created, hyperswitch_token_data, state)
.await?;
if let Some(metadata) = pma.metadata {
@ -2200,10 +2206,8 @@ async fn get_card_details(
pm: &payment_method::PaymentMethod,
key: &[u8],
state: &routes::AppState,
hyperswitch_token: &str,
key_store: &domain::MerchantKeyStore,
) -> errors::RouterResult<Option<api::CardDetailFromLocker>> {
let mut _card_decrypted =
) -> errors::RouterResult<api::CardDetailFromLocker> {
let card_decrypted =
decrypt::<serde_json::Value, masking::WithType>(pm.payment_method_data.clone(), key)
.await
.change_context(errors::StorageError::DecryptionError)
@ -2217,16 +2221,17 @@ async fn get_card_details(
_ => None,
});
Ok(Some(
get_lookup_key_from_locker(state, hyperswitch_token, pm, key_store).await?,
))
Ok(if let Some(mut crd) = card_decrypted {
crd.scheme = pm.scheme.clone();
crd
} else {
get_card_details_from_locker(state, pm).await?
})
}
pub async fn get_lookup_key_from_locker(
pub async fn get_card_details_from_locker(
state: &routes::AppState,
payment_token: &str,
pm: &storage::PaymentMethod,
merchant_key_store: &domain::MerchantKeyStore,
) -> errors::RouterResult<api::CardDetailFromLocker> {
let card = get_card_from_locker(
state,
@ -2237,9 +2242,19 @@ pub async fn get_lookup_key_from_locker(
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error getting card from card vault")?;
let card_detail = payment_methods::get_card_detail(pm, card)
payment_methods::get_card_detail(pm, card)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Get Card Details Failed")?;
.attach_printable("Get Card Details Failed")
}
pub async fn get_lookup_key_from_locker(
state: &routes::AppState,
payment_token: &str,
pm: &storage::PaymentMethod,
merchant_key_store: &domain::MerchantKeyStore,
) -> errors::RouterResult<api::CardDetailFromLocker> {
let card_detail = get_card_details_from_locker(state, pm).await?;
let card = card_detail.clone();
let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker(

View File

@ -55,7 +55,7 @@ use crate::{
utils::{
self,
crypto::{self, SignMessage},
OptionExt,
OptionExt, StringExt,
},
};
@ -1326,6 +1326,114 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>(
))
}
pub async fn retrieve_payment_method_with_temporary_token(
state: &AppState,
token: &str,
payment_intent: &PaymentIntent,
card_cvc: Option<masking::Secret<String>>,
merchant_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Option<(api::PaymentMethodData, enums::PaymentMethod)>> {
let (pm, supplementary_data) =
vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store)
.await
.attach_printable(
"Payment method for given token not found or there was a problem fetching it",
)?;
utils::when(
supplementary_data
.customer_id
.ne(&payment_intent.customer_id),
|| {
Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() })
},
)?;
Ok::<_, error_stack::Report<errors::ApiErrorResponse>>(match pm {
Some(api::PaymentMethodData::Card(card)) => {
if let Some(cvc) = card_cvc {
let mut updated_card = card;
updated_card.card_cvc = cvc;
let updated_pm = api::PaymentMethodData::Card(updated_card);
vault::Vault::store_payment_method_data_in_locker(
state,
Some(token.to_owned()),
&updated_pm,
payment_intent.customer_id.to_owned(),
enums::PaymentMethod::Card,
merchant_key_store,
)
.await?;
Some((updated_pm, enums::PaymentMethod::Card))
} else {
Some((
api::PaymentMethodData::Card(card),
enums::PaymentMethod::Card,
))
}
}
Some(the_pm @ api::PaymentMethodData::Wallet(_)) => {
Some((the_pm, enums::PaymentMethod::Wallet))
}
Some(the_pm @ api::PaymentMethodData::BankTransfer(_)) => {
Some((the_pm, enums::PaymentMethod::BankTransfer))
}
Some(the_pm @ api::PaymentMethodData::BankRedirect(_)) => {
Some((the_pm, enums::PaymentMethod::BankRedirect))
}
Some(_) => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Payment method received from locker is unsupported by locker")?,
None => None,
})
}
pub async fn retrieve_card_with_permanent_token(
state: &AppState,
token: &str,
payment_intent: &PaymentIntent,
card_cvc: Option<masking::Secret<String>>,
) -> RouterResult<api::PaymentMethodData> {
let customer_id = payment_intent
.customer_id
.as_ref()
.get_required_value("customer_id")
.change_context(errors::ApiErrorResponse::UnprocessableEntity {
message: "no customer id provided for the payment".to_string(),
})?;
let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to fetch card information from the permanent locker")?;
let api_card = api::Card {
card_number: card.card_number,
card_holder_name: card
.name_on_card
.get_required_value("name_on_card")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("card holder name was not saved in permanent locker")?,
card_exp_month: card.card_exp_month,
card_exp_year: card.card_exp_year,
card_cvc: card_cvc.unwrap_or_default(),
card_issuer: card.card_brand,
nick_name: card.nick_name.map(masking::Secret::new),
card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
};
Ok(api::PaymentMethodData::Card(api_card))
}
pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
operation: BoxedOperation<'a, F, R, Ctx>,
state: &'a AppState,
@ -1339,7 +1447,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
let token = payment_data.token.clone();
let hyperswitch_token = match payment_data.mandate_id {
Some(_) => token,
Some(_) => token.map(storage::PaymentTokenData::temporary_generic),
None => {
if let Some(token) = token {
let redis_conn = state
@ -1358,7 +1466,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
.get_required_value("payment_method")?,
);
let key = redis_conn
let token_data_string = redis_conn
.get_key::<Option<String>>(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
@ -1369,7 +1477,26 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
},
))?;
Some(key)
let token_data_result = token_data_string
.clone()
.parse_struct("PaymentTokenData")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to deserialize hyperswitch token data");
let token_data = match token_data_result {
Ok(data) => data,
Err(e) => {
// The purpose of this logic is backwards compatibility to support tokens
// in redis that might be following the old format.
if token_data_string.starts_with('{') {
return Err(e);
} else {
storage::PaymentTokenData::temporary_generic(token_data_string)
}
}
};
Some(token_data)
} else {
None
}
@ -1381,72 +1508,24 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
// TODO: Handle case where payment method and token both are present in request properly.
let payment_method = match (request, hyperswitch_token) {
(_, Some(hyperswitch_token)) => {
let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker(
let payment_method_details = Ctx::retrieve_payment_method_with_token(
state,
&hyperswitch_token,
merchant_key_store,
&hyperswitch_token,
&payment_data.payment_intent,
card_cvc,
)
.await
.attach_printable(
"Payment method for given token not found or there was a problem fetching it",
)?;
.attach_printable("in 'make_pm_data'")?;
utils::when(
supplementary_data
.customer_id
.ne(&payment_data.payment_intent.customer_id),
|| {
Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() })
Ok::<_, error_stack::Report<errors::ApiErrorResponse>>(
if let Some((payment_method_data, payment_method)) = payment_method_details {
payment_data.payment_attempt.payment_method = Some(payment_method);
Some(payment_method_data)
} else {
None
},
)?;
Ok::<_, error_stack::Report<errors::ApiErrorResponse>>(match pm.clone() {
Some(api::PaymentMethodData::Card(card)) => {
payment_data.payment_attempt.payment_method =
Some(storage_enums::PaymentMethod::Card);
if let Some(cvc) = card_cvc {
let mut updated_card = card;
updated_card.card_cvc = cvc;
let updated_pm = api::PaymentMethodData::Card(updated_card);
vault::Vault::store_payment_method_data_in_locker(
state,
Some(hyperswitch_token),
&updated_pm,
payment_data.payment_intent.customer_id.to_owned(),
enums::PaymentMethod::Card,
merchant_key_store,
)
.await?;
Some(updated_pm)
} else {
pm
}
}
Some(api::PaymentMethodData::Wallet(_)) => {
payment_data.payment_attempt.payment_method =
Some(storage_enums::PaymentMethod::Wallet);
pm
}
Some(api::PaymentMethodData::BankTransfer(_)) => {
payment_data.payment_attempt.payment_method =
Some(storage_enums::PaymentMethod::BankTransfer);
pm
}
Some(api::PaymentMethodData::BankRedirect(_)) => {
payment_data.payment_attempt.payment_method =
Some(storage_enums::PaymentMethod::BankRedirect);
pm
}
Some(_) => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable(
"Payment method received from locker is unsupported by locker",
)?,
None => None,
})
)
}
(Some(_), _) => {
@ -1495,7 +1574,11 @@ pub async fn store_in_vault_and_generate_ppmt(
});
if let Some(key_for_hyperswitch_token) = key_for_hyperswitch_token {
key_for_hyperswitch_token
.insert(Some(payment_intent.created_at), router_token, state)
.insert(
Some(payment_intent.created_at),
storage::PaymentTokenData::temporary_generic(router_token),
state,
)
.await?;
};
Ok(parent_payment_method_token)

View File

@ -9,7 +9,11 @@ use super::app::AppState;
use crate::{
core::{api_locking, errors, payment_methods::cards},
services::{api, authentication as auth},
types::api::payment_methods::{self, PaymentMethodId},
types::{
api::payment_methods::{self, PaymentMethodId},
storage::payment_method::PaymentTokenData,
},
utils::Encode,
};
/// PaymentMethods - Create
@ -379,9 +383,12 @@ impl ParentPaymentMethodToken {
pub async fn insert(
&self,
intent_created_at: Option<PrimitiveDateTime>,
token: String,
token: PaymentTokenData,
state: &AppState,
) -> CustomResult<(), errors::ApiErrorResponse> {
let token_json_str = Encode::<PaymentTokenData>::encode_to_string_of_json(&token)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to serialize hyperswitch token to json")?;
let redis_conn = state
.store
.get_redis_conn()
@ -392,7 +399,7 @@ impl ParentPaymentMethodToken {
redis_conn
.set_key_with_expiry(
&self.key_for_token,
token,
token_json_str,
TOKEN_TTL - time_elapsed.whole_seconds(),
)
.await

View File

@ -1,4 +1,44 @@
use api_models::payment_methods;
pub use diesel_models::payment_method::{
PaymentMethod, PaymentMethodNew, PaymentMethodUpdate, PaymentMethodUpdateInternal,
TokenizeCoreWorkflow,
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PaymentTokenKind {
Temporary,
Permanent,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CardTokenData {
pub token: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GenericTokenData {
pub token: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PaymentTokenData {
// The variants 'Temporary' and 'Permanent' are added for backwards compatibility
// with any tokenized data present in Redis at the time of deployment of this change
Temporary(GenericTokenData),
TemporaryGeneric(GenericTokenData),
Permanent(CardTokenData),
PermanentCard(CardTokenData),
AuthBankDebit(payment_methods::BankAccountConnectorDetails),
}
impl PaymentTokenData {
pub fn permanent_card(token: String) -> Self {
Self::PermanentCard(CardTokenData { token })
}
pub fn temporary_generic(token: String) -> Self {
Self::TemporaryGeneric(GenericTokenData { token })
}
}