Bug: Store encrypted raw card info in redis



While the customer is making a payment, we’ll check whether the extended card info feature is enabled through profile_id in the business profile. If enabled, we’ll fetch the merchant public_key and ttl from config. We’ll encrypt the card using the public key and store it in redis until ttl with the key being `{merchant_id}_{payment_id}_extended_card_info`.

diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs
index d4d1b3d80b9..fde693a3700 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<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
+    }
 }
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index d730fa8c859..3385b633699 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<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 ""
diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs
index d9d1f18c347..509056152eb 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 810143d115b..d701b64adbf 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 e2a5f148440..895e5157c00 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 d4590782b0a..c214decb0b4 100644
--- a/crates/router/src/core/payments/operations.rs
+++ b/crates/router/src/core/payments/operations.rs
@@ -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]
diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs
index 72e0903f7c3..79680cb80cc 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<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]
diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json
index 9d9d96ccac1..18ba4c6b99e 100644
--- a/openapi/openapi_spec.json
+++ b/openapi/openapi_spec.json
@@ -8707,6 +8707,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": [

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
While the customer is making a payment, we’ll check whether the extended card info feature is enabled through `profile_id` in the business profile. If enabled, we’ll fetch the merchant `public_key` and `ttl` from config. We’ll encrypt the card using the public key and store it in redis until ttl with the key being `{merchant_id}_{payment_id}_extended_card_info`.

This PR also makes the `ttl` field as an optional field. the default value would be 15 min (900 seconds) and max value would be 1 hr (3600 seconds).

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
1. Create merchant account (get default profile_id)
2. Create api key
3. Create mca (any connector)
4. Enable extended card info feature

```
curl --location 'http://localhost:8080/account/:merchant_id/business_profile/:profile_id/toggle_extended_card_info' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data '{
    "enabled": true
}'
```
![image](https://github.com/juspay/hyperswitch/assets/70657455/2c160005-4cd0-41e1-98c7-8043af5f9713)

5. Use update endpoint of business profile to pass config.
```
curl --location 'http://localhost:8080/account/:merchant_id/business_profile/:profile_id' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data '{
    "extended_card_info_config": {
        "public_key": "pub_key_1",
        "ttl_in_secs": 60
    }
}'
```
![image](https://github.com/juspay/hyperswitch/assets/70657455/67787f0e-724c-4bed-ae37-a2636093e481)

6. Create a normal card payment.
7. Hit the endpoint to retrieve the payload (PR - https://github.com/juspay/hyperswitch/pull/4484)
```
curl --location 'http://localhost:8080/payments/pay_1nrIoi2hRnzsy1V2cEv6/extended_card_info' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_KM55eCA115eJaTBYK6Z8DYLwI6EghXzZDUb9M1RzE6Ji1jMEhs71C8c43ndOubXw' \
--data ''
```

![image](https://github.com/juspay/hyperswitch/assets/70657455/d17b2d00-8787-4038-87ed-c771839e8471)


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
