From bd5356e7e7cf61f9d07fe9b67c9c5bb38fddf9c7 Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:10:21 +0530 Subject: [PATCH] refactor(core): add locker config to enable or disable locker (#3352) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 1 + crates/api_models/src/mandates.rs | 11 ++ crates/api_models/src/payment_methods.rs | 41 +++++++ crates/router/src/configs/defaults.rs | 2 + crates/router/src/configs/settings.rs | 1 + crates/router/src/core/locker_migration.rs | 4 + crates/router/src/core/mandate.rs | 18 ++-- .../router/src/core/payment_methods/cards.rs | 82 +++++++++++--- .../src/core/payment_methods/transformers.rs | 40 +++++-- .../src/core/payments/flows/authorize_flow.rs | 4 +- .../core/payments/flows/setup_mandate_flow.rs | 1 + crates/router/src/core/payments/helpers.rs | 52 +++++---- .../router/src/core/payments/tokenization.rs | 102 ++++++++++++++++-- crates/router/src/core/payouts/helpers.rs | 80 +++++++++++--- crates/router/src/core/webhooks.rs | 10 +- crates/router/src/routes/customers.rs | 7 +- crates/router/src/routes/mandates.rs | 6 +- crates/router/src/routes/payment_methods.rs | 2 +- crates/router/src/types/api/mandates.rs | 58 +++++++--- loadtest/config/development.toml | 1 + openapi/openapi_spec.json | 77 +++++++++++++ 23 files changed, 505 insertions(+), 98 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index d4e119641922..cf25ef195a24 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -131,6 +131,7 @@ host_rs = "" # Rust Locker host mock_locker = true # Emulate a locker locally using Postgres basilisk_host = "" # Basilisk host locker_signing_key_id = "1" # Key_id to sign basilisk hs locker +locker_enabled = true # Boolean to enable or disable saving cards in locker [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which has delayed session response diff --git a/config/development.toml b/config/development.toml index 91269005a0f0..b23f68680e64 100644 --- a/config/development.toml +++ b/config/development.toml @@ -69,6 +69,8 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true + [forex_api] call_delay = 21600 diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 450fe106a31f..8af1528e1771 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -56,6 +56,7 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true [jwekey] vault_encryption_key = "" diff --git a/crates/api_models/src/mandates.rs b/crates/api_models/src/mandates.rs index 5c0810dc21be..b29f4e0d0c34 100644 --- a/crates/api_models/src/mandates.rs +++ b/crates/api_models/src/mandates.rs @@ -36,6 +36,8 @@ pub struct MandateResponse { pub payment_method_id: String, /// The payment method pub payment_method: String, + /// The payment method type + pub payment_method_type: Option, /// The card details for mandate pub card: Option, /// Details about the customer’s acceptance @@ -66,6 +68,15 @@ pub struct MandateCardDetails { #[schema(value_type = Option)] /// A unique identifier alias to identify a particular card pub card_fingerprint: Option>, + /// The first 6 digits of card + pub card_isin: Option, + /// The bank that issued the card + pub card_issuer: Option, + /// The network that facilitates payment card transactions + #[schema(value_type = Option)] + pub card_network: Option, + /// The type of the payment card + pub card_type: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 3467777da745..984e6dbffff9 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -109,6 +109,19 @@ pub struct CardDetail { /// Card Holder's Nick Name #[schema(value_type = Option,example = "John Doe")] pub nick_name: Option>, + + /// Card Issuing Country + pub card_issuing_country: Option, + + /// Card's Network + #[schema(value_type = Option)] + pub card_network: Option, + + /// Issuer Bank for Card + pub card_issuer: Option, + + /// Card Type + pub card_type: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -177,6 +190,12 @@ pub struct CardDetailsPaymentMethod { pub expiry_year: Option>, pub nick_name: Option>, pub card_holder_name: Option>, + pub card_isin: Option, + pub card_issuer: Option, + pub card_network: Option, + pub card_type: Option, + #[serde(default = "saved_in_locker_default")] + pub saved_to_locker: bool, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -227,6 +246,18 @@ pub struct CardDetailFromLocker { #[schema(value_type=Option)] pub nick_name: Option>, + + #[schema(value_type = Option)] + pub card_network: Option, + + pub card_isin: Option, + pub card_issuer: Option, + pub card_type: Option, + pub saved_to_locker: bool, +} + +fn saved_in_locker_default() -> bool { + true } impl From for CardDetailFromLocker { @@ -242,6 +273,11 @@ impl From for CardDetailFromLocker { card_holder_name: item.card_holder_name, card_fingerprint: None, nick_name: item.nick_name, + card_isin: item.card_isin, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type, + saved_to_locker: item.saved_to_locker, } } } @@ -255,6 +291,11 @@ impl From for CardDetailsPaymentMethod { expiry_year: item.expiry_year, nick_name: item.nick_name, card_holder_name: item.card_holder_name, + card_isin: item.card_isin, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type, + saved_to_locker: item.saved_to_locker, } } } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 42839bf35131..e4a470d0da35 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -54,6 +54,8 @@ impl Default for super::settings::Locker { mock_locker: true, basilisk_host: "localhost".into(), locker_signing_key_id: "1".into(), + //true or false + locker_enabled: true, } } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 3d93c2f188b7..bcf26d63ae8d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -490,6 +490,7 @@ pub struct Locker { pub mock_locker: bool, pub basilisk_host: String, pub locker_signing_key_id: String, + pub locker_enabled: bool, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index e3e308a8a01c..4bd2555792a2 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -101,6 +101,10 @@ pub async fn call_to_locker( card_exp_year: card.card_exp_year, card_holder_name: card.name_on_card, nick_name: card.nick_name.map(masking::Secret::new), + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, }; let pm_create = api::PaymentMethodCreate { diff --git a/crates/router/src/core/mandate.rs b/crates/router/src/core/mandate.rs index aabd846660ca..b6837d14f829 100644 --- a/crates/router/src/core/mandate.rs +++ b/crates/router/src/core/mandate.rs @@ -33,6 +33,7 @@ use crate::{ pub async fn get_mandate( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, req: mandates::MandateId, ) -> RouterResponse { let mandate = state @@ -42,7 +43,7 @@ pub async fn get_mandate( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; Ok(services::ApplicationResponse::Json( - mandates::MandateResponse::from_db_mandate(&state, mandate).await?, + mandates::MandateResponse::from_db_mandate(&state, key_store, mandate).await?, )) } @@ -202,6 +203,7 @@ pub async fn update_connector_mandate_id( pub async fn get_customer_mandates( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, req: customers::CustomerId, ) -> RouterResponse> { let mandates = state @@ -221,7 +223,10 @@ pub async fn get_customer_mandates( } else { let mut response_vec = Vec::with_capacity(mandates.len()); for mandate in mandates { - response_vec.push(mandates::MandateResponse::from_db_mandate(&state, mandate).await?); + response_vec.push( + mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate) + .await?, + ); } Ok(services::ApplicationResponse::Json(response_vec)) } @@ -383,6 +388,7 @@ where pub async fn retrieve_mandates_list( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, constraints: api_models::mandates::MandateListConstraints, ) -> RouterResponse> { let mandates = state @@ -392,11 +398,9 @@ pub async fn retrieve_mandates_list( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to retrieve mandates")?; - let mandates_list = future::try_join_all( - mandates - .into_iter() - .map(|mandate| mandates::MandateResponse::from_db_mandate(&state, mandate)), - ) + let mandates_list = future::try_join_all(mandates.into_iter().map(|mandate| { + mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate) + })) .await?; Ok(services::ApplicationResponse::Json(mandates_list)) } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 0dbf0680d14b..712ae9e4035e 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -558,6 +558,7 @@ pub async fn add_card_hs( req, &merchant_account.merchant_id, ); + Ok(( payment_method_resp, store_card_payload.duplicate.unwrap_or(false), @@ -2508,11 +2509,19 @@ pub async fn list_customer_payment_method( let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); 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()), - ), + enums::PaymentMethod::Card => { + let card_details = get_card_details_with_locker_fallback(&pm, key, state).await?; + + if card_details.is_some() { + ( + card_details, + None, + PaymentTokenData::permanent_card(pm.payment_method_id.clone()), + ) + } else { + continue; + } + } #[cfg(feature = "payouts")] enums::PaymentMethod::BankTransfer => { @@ -2571,6 +2580,7 @@ pub async fn list_customer_payment_method( }; //Need validation for enabled payment method ,querying MCA + let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), customer_id: pm.customer_id, @@ -2700,7 +2710,38 @@ pub async fn list_customer_payment_method( Ok(services::ApplicationResponse::Json(response)) } -async fn get_card_details( +pub async fn get_card_details_with_locker_fallback( + pm: &payment_method::PaymentMethod, + key: &[u8], + state: &routes::AppState, +) -> errors::RouterResult> { + let card_decrypted = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .attach_printable("unable to decrypt card details") + .ok() + .flatten() + .map(|x| x.into_inner().expose()) + .and_then(|v| serde_json::from_value::(v).ok()) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), + _ => None, + }); + + Ok(if let Some(mut crd) = card_decrypted { + if crd.saved_to_locker { + crd.scheme = pm.scheme.clone(); + Some(crd) + } else { + None + } + } else { + Some(get_card_details_from_locker(state, pm).await?) + }) +} + +pub async fn get_card_details_without_locker_fallback( pm: &payment_method::PaymentMethod, key: &[u8], state: &routes::AppState, @@ -2979,25 +3020,32 @@ impl TempLockerCardSupport { pub async fn retrieve_payment_method( state: routes::AppState, pm: api::PaymentMethodId, + key_store: domain::MerchantKeyStore, ) -> errors::RouterResponse { let db = state.store.as_ref(); let pm = db .find_payment_method(&pm.payment_method_id) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let key = key_store.key.peek(); let card = if pm.payment_method == enums::PaymentMethod::Card { - let card = get_card_from_locker( - &state, - &pm.customer_id, - &pm.merchant_id, - &pm.payment_method_id, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting card from card vault")?; - let card_detail = payment_methods::get_card_detail(&pm, card) + let card_detail = if state.conf.locker.locker_enabled { + let card = get_card_from_locker( + &state, + &pm.customer_id, + &pm.merchant_id, + &pm.payment_method_id, + ) + .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details from locker")?; + .attach_printable("Error getting card from card vault")?; + payment_methods::get_card_detail(&pm, card) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card details from locker")? + } else { + get_card_details_without_locker_fallback(&pm, key, &state).await? + }; Some(card_detail) } else { None diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index da4f03b49c1e..304091e42ac7 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -352,18 +352,26 @@ pub fn mk_add_card_response_hs( req: api::PaymentMethodCreate, merchant_id: &str, ) -> api::PaymentMethodResponse { - let mut card_number = card.card_number.peek().to_owned(); + let card_number = card.card_number.clone(); + let last4_digits = card_number.clone().get_last4(); + let card_isin = card_number.get_card_isin(); + let card = api::CardDetailFromLocker { scheme: None, - last4_digits: Some(card_number.split_off(card_number.len() - 4)), - issuer_country: None, // [#256] bin mapping - card_number: Some(card.card_number), - expiry_month: Some(card.card_exp_month), - expiry_year: Some(card.card_exp_year), - card_token: None, // [#256] - card_fingerprint: None, // fingerprint not send by basilisk-hs need to have this feature in case we need it in future - card_holder_name: card.card_holder_name, - nick_name: card.nick_name, + last4_digits: Some(last4_digits), + issuer_country: None, + card_number: Some(card.card_number.clone()), + expiry_month: Some(card.card_exp_month.clone()), + expiry_year: Some(card.card_exp_year.clone()), + card_token: None, + card_fingerprint: None, + card_holder_name: card.card_holder_name.clone(), + nick_name: card.nick_name.clone(), + card_isin: Some(card_isin), + card_issuer: card.card_issuer, + card_network: card.card_network, + card_type: card.card_type, + saved_to_locker: true, }; api::PaymentMethodResponse { merchant_id: merchant_id.to_owned(), @@ -399,6 +407,11 @@ pub fn mk_add_card_response( card_fingerprint: Some(response.card_fingerprint), card_holder_name: card.card_holder_name, nick_name: card.nick_name, + card_isin: None, + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, }; api::PaymentMethodResponse { merchant_id: merchant_id.to_owned(), @@ -597,6 +610,8 @@ pub fn get_card_detail( ) -> CustomResult { let card_number = response.card_number; let mut last4_digits = card_number.peek().to_owned(); + //fetch form card bin + let card_detail = api::CardDetailFromLocker { scheme: pm.scheme.to_owned(), issuer_country: pm.issuer_country.clone(), @@ -608,6 +623,11 @@ pub fn get_card_detail( card_fingerprint: None, card_holder_name: response.name_on_card, nick_name: response.nick_name.map(masking::Secret::new), + card_isin: None, + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, }; Ok(card_detail) } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 07af15a336d9..15c79f4b9d95 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -92,7 +92,9 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics - if resp.request.setup_mandate_details.clone().is_some() { + let is_mandate = resp.request.setup_mandate_details.is_some(); + + if is_mandate { let payment_method_id = Box::pin(tokenization::save_payment_method( state, connector, diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index 0c03c8ce123b..d6343ed871b0 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -208,6 +208,7 @@ impl types::SetupMandateRouterData { .to_setup_mandate_failed_response()?; let payment_method_type = self.request.payment_method_type; + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 46e1e15fe717..9d3da6c78e4a 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -509,16 +509,23 @@ pub async fn get_token_for_recurring_mandate( }; if let diesel_models::enums::PaymentMethod::Card = payment_method.payment_method { - let _ = - cards::get_lookup_key_from_locker(state, &token, &payment_method, merchant_key_store) - .await?; + if state.conf.locker.locker_enabled { + let _ = cards::get_lookup_key_from_locker( + state, + &token, + &payment_method, + merchant_key_store, + ) + .await?; + } + if let Some(payment_method_from_request) = req.payment_method { let pm: storage_enums::PaymentMethod = payment_method_from_request; if pm != payment_method.payment_method { Err(report!(errors::ApiErrorResponse::PreconditionFailed { message: "payment method in request does not match previously provided payment \ - method information" + method information" .into() }))? } @@ -971,7 +978,6 @@ pub fn payment_intent_status_fsm( None => storage_enums::IntentStatus::RequiresPaymentMethod, } } - pub async fn add_domain_task_to_pt( operation: &Op, state: &AppState, @@ -1050,6 +1056,10 @@ pub(crate) async fn get_payment_method_create_request( card_exp_year: card.card_exp_year.clone(), card_holder_name: card.card_holder_name.clone(), nick_name: card.nick_name.clone(), + card_issuing_country: card.card_issuing_country.clone(), + card_network: card.card_network.clone(), + card_issuer: card.card_issuer.clone(), + card_type: card.card_type.clone(), }; let customer_id = customer.customer_id.clone(); let payment_method_request = api::PaymentMethodCreate { @@ -3359,21 +3369,23 @@ pub async fn get_additional_payment_data( }, )) }); - card_info.unwrap_or(api_models::payments::AdditionalPaymentData::Card(Box::new( - api_models::payments::AdditionalCardInfo { - card_issuer: None, - card_network: None, - bank_code: None, - card_type: None, - card_issuing_country: None, - last4, - card_isin, - card_extended_bin, - card_exp_month: Some(card_data.card_exp_month.clone()), - card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: card_data.card_holder_name.clone(), - }, - ))) + card_info.unwrap_or_else(|| { + api_models::payments::AdditionalPaymentData::Card(Box::new( + api_models::payments::AdditionalCardInfo { + card_issuer: None, + card_network: None, + bank_code: None, + card_type: None, + card_issuing_country: None, + last4, + card_isin, + card_extended_bin, + card_exp_month: Some(card_data.card_exp_month.clone()), + card_exp_year: Some(card_data.card_exp_year.clone()), + card_holder_name: card_data.card_holder_name.clone(), + }, + )) + }) } } api_models::payments::PaymentMethodData::BankRedirect(bank_redirect_data) => { diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index f884cb79e7e1..15d88c94660c 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -14,7 +14,7 @@ use crate::{ services, types::{ self, - api::{self, CardDetailsPaymentMethod, PaymentMethodCreateExt}, + api::{self, CardDetailFromLocker, CardDetailsPaymentMethod, PaymentMethodCreateExt}, domain, storage::enums as storage_enums, }, @@ -74,12 +74,21 @@ where .await?; let merchant_id = &merchant_account.merchant_id; - let locker_response = save_in_locker( - state, - merchant_account, - payment_method_create_request.to_owned(), - ) - .await?; + let locker_response = if !state.conf.locker.locker_enabled { + skip_saving_card_in_locker( + merchant_account, + payment_method_create_request.to_owned(), + ) + .await? + } else { + save_in_locker( + state, + merchant_account, + payment_method_create_request.to_owned(), + ) + .await? + }; + let is_duplicate = locker_response.1; let pm_card_details = locker_response.0.card.as_ref().map(|card| { @@ -168,6 +177,85 @@ where } } +async fn skip_saving_card_in_locker( + merchant_account: &domain::MerchantAccount, + payment_method_request: api::PaymentMethodCreate, +) -> RouterResult<(api_models::payment_methods::PaymentMethodResponse, bool)> { + let merchant_id = &merchant_account.merchant_id; + let customer_id = payment_method_request + .clone() + .customer_id + .clone() + .get_required_value("customer_id")?; + let payment_method_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm"); + + let last4_digits = payment_method_request + .card + .clone() + .map(|c| c.card_number.get_last4()); + + let card_isin = payment_method_request + .card + .clone() + .map(|c: api_models::payment_methods::CardDetail| c.card_number.get_card_isin()); + + match payment_method_request.card.clone() { + Some(card) => { + let card_detail = CardDetailFromLocker { + scheme: None, + issuer_country: card.card_issuing_country.clone(), + last4_digits: last4_digits.clone(), + card_number: None, + expiry_month: Some(card.card_exp_month.clone()), + expiry_year: Some(card.card_exp_year), + card_token: None, + card_holder_name: card.card_holder_name.clone(), + card_fingerprint: None, + nick_name: None, + card_isin: card_isin.clone(), + card_issuer: card.card_issuer.clone(), + card_network: card.card_network.clone(), + card_type: card.card_type.clone(), + saved_to_locker: false, + }; + let pm_resp = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id), + payment_method_id, + payment_method: payment_method_request.payment_method, + payment_method_type: payment_method_request.payment_method_type, + card: Some(card_detail), + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + metadata: None, + created: Some(common_utils::date_time::now()), + bank_transfer: None, + }; + + Ok((pm_resp, false)) + } + None => { + let pm_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm"); + let payment_method_response = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id), + payment_method_id: pm_id, + payment_method: payment_method_request.payment_method, + payment_method_type: payment_method_request.payment_method_type, + card: None, + metadata: None, + created: Some(common_utils::date_time::now()), + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + bank_transfer: None, + }; + Ok((payment_method_response, false)) + } + } +} + pub async fn save_in_locker( state: &AppState, merchant_account: &domain::MerchantAccount, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 56e3a6faf537..1ab24023bdb8 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1,6 +1,6 @@ use common_utils::{ errors::CustomResult, - ext_traits::{StringExt, ValueExt}, + ext_traits::{AsyncExt, StringExt, ValueExt}, }; use diesel_models::encryption::Encryption; use error_stack::{IntoReport, ResultExt}; @@ -19,6 +19,7 @@ use crate::{ }, db::StorageInterface, routes::AppState, + services, types::{ api::{self, enums as api_enums}, domain::{ @@ -184,6 +185,10 @@ pub async fn save_payout_data_to_locker( card_exp_month: card.expiry_month.to_owned(), card_exp_year: card.expiry_year.to_owned(), nick_name: None, + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, }; let payload = StoreLockerReq::LockerCard(StoreCardReq { merchant_id: &merchant_account.merchant_id, @@ -267,20 +272,65 @@ pub async fn save_payout_data_to_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payouts in saved payout method")?; - let pm_data = api::payment_methods::PaymentMethodsData::Card( - api::payment_methods::CardDetailsPaymentMethod { - last4_digits: card_details - .as_ref() - .map(|c| c.card_number.clone().get_last4()), - issuer_country: None, - expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), - expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), - nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), - card_holder_name: card_details - .as_ref() - .and_then(|c| c.card_holder_name.clone()), - }, - ); + // fetch card info from db + let card_isin = card_details + .as_ref() + .map(|c| c.card_number.clone().get_card_isin()); + + let pm_data = card_isin + .clone() + .async_and_then(|card_isin| async move { + db.get_card_info(&card_isin) + .await + .map_err(|error| services::logger::warn!(card_info_error=?error)) + .ok() + }) + .await + .flatten() + .map(|card_info| { + api::payment_methods::PaymentMethodsData::Card( + api::payment_methods::CardDetailsPaymentMethod { + last4_digits: card_details + .as_ref() + .map(|c| c.card_number.clone().get_last4()), + issuer_country: card_info.card_issuing_country, + expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), + expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), + nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), + card_holder_name: card_details + .as_ref() + .and_then(|c| c.card_holder_name.clone()), + + card_isin: card_isin.clone(), + card_issuer: card_info.card_issuer, + card_network: card_info.card_network, + card_type: card_info.card_type, + saved_to_locker: true, + }, + ) + }) + .unwrap_or_else(|| { + api::payment_methods::PaymentMethodsData::Card( + api::payment_methods::CardDetailsPaymentMethod { + last4_digits: card_details + .as_ref() + .map(|c| c.card_number.clone().get_last4()), + issuer_country: None, + expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), + expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), + nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), + card_holder_name: card_details + .as_ref() + .and_then(|c| c.card_holder_name.clone()), + + card_isin: card_isin.clone(), + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, + }, + ) + }); let card_details_encrypted = cards::create_encrypted_payment_method_data(key_store, Some(pm_data)).await; diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 4354a3ee1959..f291d1cd2e80 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -421,6 +421,7 @@ pub async fn mandates_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, + key_store: domain::MerchantKeyStore, webhook_details: api::IncomingWebhookDetails, source_verified: bool, event_type: api_models::webhooks::IncomingWebhookEvent, @@ -464,8 +465,12 @@ pub async fn mandates_incoming_webhook_flow( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; let mandates_response = Box::new( - api::mandates::MandateResponse::from_db_mandate(&state, updated_mandate.clone()) - .await?, + api::mandates::MandateResponse::from_db_mandate( + &state, + key_store, + updated_mandate.clone(), + ) + .await?, ); let event_type: Option = updated_mandate.mandate_status.foreign_into(); if let Some(outgoing_event_type) = event_type { @@ -1237,6 +1242,7 @@ pub async fn webhooks_core RouterResult; + async fn from_db_mandate( + state: &AppState, + key_store: domain::MerchantKeyStore, + mandate: storage::Mandate, + ) -> RouterResult; } #[async_trait::async_trait] impl MandateResponseExt for MandateResponse { - async fn from_db_mandate(state: &AppState, mandate: storage::Mandate) -> RouterResult { + async fn from_db_mandate( + state: &AppState, + key_store: domain::MerchantKeyStore, + mandate: storage::Mandate, + ) -> RouterResult { let db = &*state.store; let payment_method = db .find_payment_method(&mandate.payment_method_id) @@ -36,21 +45,35 @@ impl MandateResponseExt for MandateResponse { .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let card = if payment_method.payment_method == storage_enums::PaymentMethod::Card { - let card = payment_methods::cards::get_card_from_locker( - state, - &payment_method.customer_id, - &payment_method.merchant_id, - &payment_method.payment_method_id, - ) - .await?; - let card_detail = payment_methods::transformers::get_card_detail(&payment_method, card) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details")?; - Some(MandateCardDetails::from(card_detail).into_inner()) + // if locker is disabled , decrypt the payment method data + let card_details = if state.conf.locker.locker_enabled { + let card = payment_methods::cards::get_card_from_locker( + state, + &payment_method.customer_id, + &payment_method.merchant_id, + &payment_method.payment_method_id, + ) + .await?; + + payment_methods::transformers::get_card_detail(&payment_method, card) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card details")? + } else { + payment_methods::cards::get_card_details_without_locker_fallback( + &payment_method, + key_store.key.get_inner().peek(), + state, + ) + .await? + }; + + Some(MandateCardDetails::from(card_details).into_inner()) } else { None }; - + let payment_method_type = payment_method + .payment_method_type + .map(|pmt| pmt.to_string()); Ok(Self { mandate_id: mandate.mandate_id, customer_acceptance: Some(api::payments::CustomerAcceptance { @@ -68,6 +91,7 @@ impl MandateResponseExt for MandateResponse { card, status: mandate.mandate_status, payment_method: payment_method.payment_method.to_string(), + payment_method_type, payment_method_id: mandate.payment_method_id, }) } @@ -84,6 +108,10 @@ impl From for MandateCardDetails { scheme: card_details_from_locker.scheme, issuer_country: card_details_from_locker.issuer_country, card_fingerprint: card_details_from_locker.card_fingerprint, + card_isin: card_details_from_locker.card_isin, + card_issuer: card_details_from_locker.card_issuer, + card_network: card_details_from_locker.card_network, + card_type: card_details_from_locker.card_type, } .into() } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 358a591a6678..268ebd1d3ac9 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -33,6 +33,7 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true [forex_api] call_delay = 21600 diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index b2f5d3ea52c3..02df6324a06d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4421,11 +4421,37 @@ "description": "Card Holder's Nick Name", "example": "John Doe", "nullable": true + }, + "card_issuing_country": { + "type": "string", + "description": "Card Issuing Country", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_issuer": { + "type": "string", + "description": "Issuer Bank for Card", + "nullable": true + }, + "card_type": { + "type": "string", + "description": "Card Type", + "nullable": true } } }, "CardDetailFromLocker": { "type": "object", + "required": [ + "saved_to_locker" + ], "properties": { "scheme": { "type": "string", @@ -4462,6 +4488,29 @@ "nick_name": { "type": "string", "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_isin": { + "type": "string", + "nullable": true + }, + "card_issuer": { + "type": "string", + "nullable": true + }, + "card_type": { + "type": "string", + "nullable": true + }, + "saved_to_locker": { + "type": "boolean" } } }, @@ -6884,6 +6933,29 @@ "type": "string", "description": "A unique identifier alias to identify a particular card", "nullable": true + }, + "card_isin": { + "type": "string", + "description": "The first 6 digits of card", + "nullable": true + }, + "card_issuer": { + "type": "string", + "description": "The bank that issued the card", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_type": { + "type": "string", + "description": "The type of the payment card", + "nullable": true } } }, @@ -6932,6 +7004,11 @@ "type": "string", "description": "The payment method" }, + "payment_method_type": { + "type": "string", + "description": "The payment method type", + "nullable": true + }, "card": { "allOf": [ {