From 6c59d2434ce5067611d85d37b7ec6f551b7ad81a Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 2 May 2024 16:58:44 +0530 Subject: [PATCH] feat: store encrypted extended card info in redis (#4493) --- crates/api_models/src/admin.rs | 44 +++++++++- crates/api_models/src/payments.rs | 52 ++++++++++++ crates/common_utils/src/consts.rs | 6 ++ crates/openapi/src/openapi.rs | 2 + crates/router/src/core/payments.rs | 10 +++ crates/router/src/core/payments/operations.rs | 10 +++ .../payments/operations/payment_confirm.rs | 67 +++++++++++++++- openapi/openapi_spec.json | 80 +++++++++++++++++++ 8 files changed, 268 insertions(+), 3 deletions(-) diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index d4d1b3d80b..fde693a370 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use common_utils::{ + consts, crypto::{Encryptable, OptionalEncryptableName}, pii, }; @@ -1099,8 +1100,47 @@ pub struct ExtendedCardInfoChoice { impl common_utils::events::ApiEventMetric for ExtendedCardInfoChoice {} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ExtendedCardInfoConfig { + /// Merchant public key + #[schema(value_type = String)] pub public_key: Secret, - pub ttl_in_secs: u16, + /// TTL for extended card info + #[schema(default = 900, maximum = 3600, value_type = u16)] + #[serde(default)] + pub ttl_in_secs: TtlForExtendedCardInfo, +} + +#[derive(Debug, serde::Serialize, Clone)] +pub struct TtlForExtendedCardInfo(u16); + +impl Default for TtlForExtendedCardInfo { + fn default() -> Self { + Self(consts::DEFAULT_TTL_FOR_EXTENDED_CARD_INFO) + } +} + +impl<'de> Deserialize<'de> for TtlForExtendedCardInfo { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = u16::deserialize(deserializer)?; + + // Check if value exceeds the maximum allowed + if value > consts::MAX_TTL_FOR_EXTENDED_CARD_INFO { + Err(serde::de::Error::custom( + "ttl_in_secs must be less than or equal to 3600 (1hr)", + )) + } else { + Ok(Self(value)) + } + } +} + +impl std::ops::Deref for TtlForExtendedCardInfo { + type Target = u16; + fn deref(&self) -> &Self::Target { + &self.0 + } } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index d730fa8c85..3385b63369 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -916,6 +916,58 @@ pub struct Card { pub nick_name: Option>, } +#[derive(Default, Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct ExtendedCardInfo { + /// The card number + #[schema(value_type = String, example = "4242424242424242")] + pub card_number: CardNumber, + + /// The card's expiry month + #[schema(value_type = String, example = "24")] + pub card_exp_month: Secret, + + /// The card's expiry year + #[schema(value_type = String, example = "24")] + pub card_exp_year: Secret, + + /// The card holder's name + #[schema(value_type = String, example = "John Test")] + pub card_holder_name: Option>, + + /// The name of the issuer of card + #[schema(example = "chase")] + pub card_issuer: Option, + + /// The card network for the card + #[schema(value_type = Option, example = "Visa")] + pub card_network: Option, + + #[schema(example = "CREDIT")] + pub card_type: Option, + + #[schema(example = "INDIA")] + pub card_issuing_country: Option, + + #[schema(example = "JP_AMEX")] + pub bank_code: Option, +} + +impl From for ExtendedCardInfo { + fn from(value: Card) -> Self { + Self { + card_number: value.card_number, + card_exp_month: value.card_exp_month, + card_exp_year: value.card_exp_year, + card_holder_name: value.card_holder_name, + card_issuer: value.card_issuer, + card_network: value.card_network, + card_type: value.card_type, + card_issuing_country: value.card_issuing_country, + bank_code: value.bank_code, + } + } +} + impl GetAddressFromPaymentMethodData for Card { fn get_billing_address(&self) -> Option
{ // Create billing address if first_name is some or if it is not "" diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index d9d1f18c34..509056152e 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -77,3 +77,9 @@ pub const DEFAULT_DISPLAY_SDK_ONLY: bool = false; /// Default bool to enable saved payment method pub const DEFAULT_ENABLE_SAVED_PAYMENT_METHOD: bool = false; + +/// Default ttl for Extended card info in redis (in seconds) +pub const DEFAULT_TTL_FOR_EXTENDED_CARD_INFO: u16 = 15 * 60; + +/// Max ttl for Extended card info in redis (in seconds) +pub const MAX_TTL_FOR_EXTENDED_CARD_INFO: u16 = 60 * 60; diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 1e6746a87a..1e38440839 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -194,6 +194,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::MerchantConnectorDeleteResponse, api_models::admin::MerchantConnectorResponse, api_models::admin::AuthenticationConnectorDetails, + api_models::admin::ExtendedCardInfoConfig, api_models::customers::CustomerRequest, api_models::customers::CustomerDeleteResponse, api_models::payment_methods::PaymentMethodCreate, @@ -407,6 +408,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ThreeDsMethodData, api_models::payments::PollConfigResponse, api_models::payments::ExternalAuthenticationDetailsResponse, + api_models::payments::ExtendedCardInfo, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::DefaultPaymentMethod, api_models::payment_methods::MaskedBankDetails, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e2a5f14844..895e5157c0 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -527,6 +527,16 @@ where let cloned_payment_data = payment_data.clone(); let cloned_customer = customer.clone(); + operation + .to_domain()? + .store_extended_card_info_temporarily( + state, + &payment_data.payment_intent.payment_id, + &business_profile, + &payment_data.payment_method_data, + ) + .await?; + crate::utils::trigger_payments_webhook( merchant_account, business_profile, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index d4590782b0..c214decb0b 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -190,6 +190,16 @@ pub trait Domain: Send + Sync { ) -> CustomResult { Ok(false) } + + async fn store_extended_card_info_temporarily<'a>( + &'a self, + _state: &AppState, + _payment_id: &str, + _business_profile: &storage::BusinessProfile, + _payment_method_data: &Option, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 72e0903f7c..79680cb80c 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,10 +1,11 @@ use std::marker::PhantomData; -use api_models::enums::FrmSuggestion; +use api_models::{admin::ExtendedCardInfoConfig, enums::FrmSuggestion, payments::ExtendedCardInfo}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use error_stack::{report, ResultExt}; use futures::FutureExt; +use masking::{ExposeInterface, PeekInterface}; use router_derive::PaymentOperation; use router_env::{instrument, logger, tracing}; use tracing_futures::Instrument; @@ -895,6 +896,70 @@ impl Domain CustomResult { blocklist_utils::validate_data_for_blocklist(state, merchant_account, payment_data).await } + + #[instrument(skip_all)] + async fn store_extended_card_info_temporarily<'a>( + &'a self, + state: &AppState, + payment_id: &str, + business_profile: &storage::BusinessProfile, + payment_method_data: &Option, + ) -> CustomResult<(), errors::ApiErrorResponse> { + if let (Some(true), Some(api::PaymentMethodData::Card(card)), Some(merchant_config)) = ( + business_profile.is_extended_card_info_enabled, + payment_method_data, + business_profile.extended_card_info_config.clone(), + ) { + let merchant_config = merchant_config + .expose() + .parse_value::("ExtendedCardInfoConfig") + .map_err(|err| logger::error!(parse_err=?err,"Error while parsing ExtendedCardInfoConfig")); + + let card_data = ExtendedCardInfo::from(card.clone()) + .encode_to_vec() + .map_err(|err| logger::error!(encode_err=?err,"Error while encoding ExtendedCardInfo to vec")); + + let (Ok(merchant_config), Ok(card_data)) = (merchant_config, card_data) else { + return Ok(()); + }; + + let encrypted_payload = + services::encrypt_jwe(&card_data, merchant_config.public_key.peek()) + .await + .map_err(|err| { + logger::error!(jwe_encryption_err=?err,"Error while JWE encrypting extended card info") + }); + + let Ok(encrypted_payload) = encrypted_payload else { + return Ok(()); + }; + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let key = helpers::get_redis_key_for_extended_card_info( + &business_profile.merchant_id, + payment_id, + ); + + redis_conn + .set_key_with_expiry( + &key, + encrypted_payload.clone(), + (*merchant_config.ttl_in_secs).into(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add extended card info in redis")?; + + logger::info!("Extended card info added to redis"); + } + + Ok(()) + } } #[async_trait] diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 90d81793d2..d795387853 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8711,6 +8711,86 @@ "mandate_revoked" ] }, + "ExtendedCardInfo": { + "type": "object", + "required": [ + "card_number", + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_number": { + "type": "string", + "description": "The card number", + "example": "4242424242424242" + }, + "card_exp_month": { + "type": "string", + "description": "The card's expiry month", + "example": "24" + }, + "card_exp_year": { + "type": "string", + "description": "The card's expiry year", + "example": "24" + }, + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Test" + }, + "card_issuer": { + "type": "string", + "description": "The name of the issuer of card", + "example": "chase", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_type": { + "type": "string", + "example": "CREDIT", + "nullable": true + }, + "card_issuing_country": { + "type": "string", + "example": "INDIA", + "nullable": true + }, + "bank_code": { + "type": "string", + "example": "JP_AMEX", + "nullable": true + } + } + }, + "ExtendedCardInfoConfig": { + "type": "object", + "required": [ + "public_key" + ], + "properties": { + "public_key": { + "type": "string", + "description": "Merchant public key" + }, + "ttl_in_secs": { + "type": "integer", + "format": "int32", + "description": "TTL for extended card info", + "default": 900, + "maximum": 3600, + "minimum": 0 + } + } + }, "ExtendedCardInfoResponse": { "type": "object", "required": [