From efeebc0f2365f0900de3dd3e10a1539621c9933d Mon Sep 17 00:00:00 2001 From: Shanks Date: Mon, 20 Nov 2023 16:12:06 +0530 Subject: [PATCH] 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> --- crates/router/src/core/errors.rs | 2 +- crates/router/src/core/payment_methods.rs | 77 ++++++- .../router/src/core/payment_methods/cards.rs | 77 ++++--- crates/router/src/core/payments/helpers.rs | 213 ++++++++++++------ crates/router/src/routes/payment_methods.rs | 13 +- .../src/types/storage/payment_method.rs | 40 ++++ 6 files changed, 320 insertions(+), 102 deletions(-) diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 810c079987..03bb9a41b5 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -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::*, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index b19b381af5..0628d30179 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -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, Option)>; + + async fn retrieve_payment_method_with_token( + state: &AppState, + key_store: &domain::MerchantKeyStore, + token: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + ) -> RouterResult>; } #[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>, + ) -> RouterResult> { + 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() + } + } + } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 80daf66a69..f2eeedf538 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -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> { - let mut _card_decrypted = +) -> errors::RouterResult { + let card_decrypted = decrypt::(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 { 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 { + 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( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index cd056f81eb..fb74006a06 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -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>, + merchant_key_store: &domain::MerchantKeyStore, +) -> RouterResult> { + 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>(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>, +) -> RouterResult { + 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::>(&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>( + 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>(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) diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 83d4c7f966..43a7272a44 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -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, - token: String, + token: PaymentTokenData, state: &AppState, ) -> CustomResult<(), errors::ApiErrorResponse> { + let token_json_str = Encode::::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 diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 737e6f6607..096303446d 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -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 }) + } +}