mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat: store encrypted extended card info in redis (#4493)
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user