feat: store encrypted extended card info in redis (#4493)

This commit is contained in:
Chethan Rao
2024-05-02 16:58:44 +05:30
committed by GitHub
parent 5a447afd74
commit 6c59d2434c
8 changed files with 268 additions and 3 deletions

View File

@ -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<String>,
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<D>(deserializer: D) -> Result<Self, D::Error>
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
}
}

View File

@ -916,6 +916,58 @@ pub struct Card {
pub nick_name: Option<Secret<String>>,
}
#[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<String>,
/// The card's expiry year
#[schema(value_type = String, example = "24")]
pub card_exp_year: Secret<String>,
/// The card holder's name
#[schema(value_type = String, example = "John Test")]
pub card_holder_name: Option<Secret<String>>,
/// The name of the issuer of card
#[schema(example = "chase")]
pub card_issuer: Option<String>,
/// The card network for the card
#[schema(value_type = Option<CardNetwork>, example = "Visa")]
pub card_network: Option<api_enums::CardNetwork>,
#[schema(example = "CREDIT")]
pub card_type: Option<String>,
#[schema(example = "INDIA")]
pub card_issuing_country: Option<String>,
#[schema(example = "JP_AMEX")]
pub bank_code: Option<String>,
}
impl From<Card> 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<Address> {
// Create billing address if first_name is some or if it is not ""

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -190,6 +190,16 @@ pub trait Domain<F: Clone, R, Ctx: PaymentMethodRetrieve>: Send + Sync {
) -> CustomResult<bool, errors::ApiErrorResponse> {
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<api::PaymentMethodData>,
) -> CustomResult<(), errors::ApiErrorResponse> {
Ok(())
}
}
#[async_trait]

View File

@ -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<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
) -> CustomResult<bool, errors::ApiErrorResponse> {
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<api::PaymentMethodData>,
) -> 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>("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]

View File

@ -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": [