Bug: extended_card_info API to retrieve encrypted card PAN



New endpoint to retrieve the extended card info from redis given payment_id and api_key as auth
diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs
index b8f4a9d6d90..59e65c0605f 100644
--- a/crates/api_models/src/events/payment.rs
+++ b/crates/api_models/src/events/payment.rs
@@ -8,9 +8,10 @@ use crate::{
         PaymentMethodResponse, PaymentMethodUpdate,
     },
     payments::{
-        PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters,
-        PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest,
-        PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsExternalAuthenticationRequest,
+        ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints,
+        PaymentListFilterConstraints, PaymentListFilters, PaymentListFiltersV2,
+        PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest,
+        PaymentsCaptureRequest, PaymentsExternalAuthenticationRequest,
         PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest,
         PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest,
         PaymentsStartRequest, RedirectionResponse,
@@ -201,3 +202,5 @@ impl ApiEventMetric for PaymentsExternalAuthenticationRequest {
         })
     }
 }
+
+impl ApiEventMetric for ExtendedCardInfoResponse {}
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index 5d35ba3e490..bd19443d002 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -4622,6 +4622,12 @@ pub enum PaymentLinkStatusWrap {
     IntentStatus(api_enums::IntentStatus),
 }
 
+#[derive(Debug, Default, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
+pub struct ExtendedCardInfoResponse {
+    // Encrypted customer payment method data
+    pub payload: String,
+}
+
 #[cfg(test)]
 mod payments_request_api_contract {
     #![allow(clippy::unwrap_used)]
diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs
index bb9ef731992..810143d115b 100644
--- a/crates/openapi/src/openapi.rs
+++ b/crates/openapi/src/openapi.rs
@@ -470,6 +470,7 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::payments::PaymentLinkResponse,
         api_models::payments::RetrievePaymentLinkResponse,
         api_models::payments::PaymentLinkInitiateRequest,
+        api_models::payments::ExtendedCardInfoResponse,
         api_models::routing::RoutingConfigRequest,
         api_models::routing::RoutingDictionaryRecord,
         api_models::routing::RoutingKind,
diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs
index 580eb4a80bc..bca1cbb64b8 100644
--- a/crates/router/src/compatibility/stripe/errors.rs
+++ b/crates/router/src/compatibility/stripe/errors.rs
@@ -262,6 +262,8 @@ pub enum StripeErrorCode {
     CurrencyConversionFailed,
     #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_25", message = "Cannot delete the default payment method")]
     PaymentMethodDeleteFailed,
+    #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "Extended card info does not exist")]
+    ExtendedCardInfoNotFound,
     // [#216]: https://github.com/juspay/hyperswitch/issues/216
     // Implement the remaining stripe error codes
 
@@ -643,6 +645,7 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
             errors::ApiErrorResponse::InvalidWalletToken { wallet_name } => {
                 Self::InvalidWalletToken { wallet_name }
             }
+            errors::ApiErrorResponse::ExtendedCardInfoNotFound => Self::ExtendedCardInfoNotFound,
         }
     }
 }
@@ -714,7 +717,8 @@ impl actix_web::ResponseError for StripeErrorCode {
             | Self::PaymentMethodUnactivated
             | Self::InvalidConnectorConfiguration { .. }
             | Self::CurrencyConversionFailed
-            | Self::PaymentMethodDeleteFailed => StatusCode::BAD_REQUEST,
+            | Self::PaymentMethodDeleteFailed
+            | Self::ExtendedCardInfoNotFound => StatusCode::BAD_REQUEST,
             Self::RefundFailed
             | Self::PayoutFailed
             | Self::PaymentLinkNotFound
diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs
index 5d1e527d6d4..0c492f0f089 100644
--- a/crates/router/src/core/errors/api_error_response.rs
+++ b/crates/router/src/core/errors/api_error_response.rs
@@ -269,6 +269,8 @@ pub enum ApiErrorResponse {
         message = "Invalid Cookie"
     )]
     InvalidCookie,
+    #[error(error_type = ErrorType::InvalidRequestError, code = "IR_27", message = "Extended card info does not exist")]
+    ExtendedCardInfoNotFound,
 }
 
 impl PTError for ApiErrorResponse {
diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs
index 1197e01cdf4..880c0d7b20b 100644
--- a/crates/router/src/core/errors/transformers.rs
+++ b/crates/router/src/core/errors/transformers.rs
@@ -301,6 +301,9 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> for ApiErrorRespon
             Self::InvalidCookie => {
                 AER::BadRequest(ApiError::new("IR", 26, "Invalid Cookie", None))
             }
+            Self::ExtendedCardInfoNotFound => {
+                AER::NotFound(ApiError::new("IR", 27, "Extended card info does not exist", None))
+            }
         }
     }
 }
diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs
index 67f095450c0..3f775397b19 100644
--- a/crates/router/src/core/payments.rs
+++ b/crates/router/src/core/payments.rs
@@ -3992,3 +3992,26 @@ pub async fn payment_external_authentication(
         },
     ))
 }
+
+#[instrument(skip_all)]
+pub async fn get_extended_card_info(
+    state: AppState,
+    merchant_id: String,
+    payment_id: String,
+) -> RouterResponse<payments_api::ExtendedCardInfoResponse> {
+    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(&merchant_id, &payment_id);
+    let payload = redis_conn
+        .get_key::<String>(&key)
+        .await
+        .change_context(errors::ApiErrorResponse::ExtendedCardInfoNotFound)?;
+
+    Ok(services::ApplicationResponse::Json(
+        payments_api::ExtendedCardInfoResponse { payload },
+    ))
+}
diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs
index 6a1a779fd1f..c82f73e2f7b 100644
--- a/crates/router/src/core/payments/helpers.rs
+++ b/crates/router/src/core/payments/helpers.rs
@@ -4320,3 +4320,7 @@ pub fn validate_mandate_data_and_future_usage(
         Ok(())
     }
 }
+
+pub fn get_redis_key_for_extended_card_info(merchant_id: &str, payment_id: &str) -> String {
+    format!("{merchant_id}_{payment_id}_extended_card_info")
+}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index a7c6f1486d3..1e9df096279 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -382,6 +382,9 @@ impl Payments {
                 )
                 .service(
                     web::resource("/{payment_id}/3ds/authentication").route(web::post().to(payments_external_authentication)),
+                )
+                .service(
+                    web::resource("/{payment_id}/extended_card_info").route(web::get().to(retrieve_extended_card_info)),
                 );
         }
         route
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index c7b1b28f2eb..8bfbd0ad0f6 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -120,7 +120,8 @@ impl From<Flow> for ApiIdentifier {
             | Flow::PaymentsRedirect
             | Flow::PaymentsIncrementalAuthorization
             | Flow::PaymentsExternalAuthentication
-            | Flow::PaymentsAuthorize => Self::Payments,
+            | Flow::PaymentsAuthorize
+            | Flow::GetExtendedCardInfo => Self::Payments,
 
             Flow::PayoutsCreate
             | Flow::PayoutsRetrieve
diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs
index c1d67c4ce00..c9e1cf14ce3 100644
--- a/crates/router/src/routes/payments.rs
+++ b/crates/router/src/routes/payments.rs
@@ -1357,6 +1357,30 @@ pub async fn post_3ds_payments_authorize(
     .await
 }
 
+/// Retrieve endpoint for merchant to fetch the encrypted customer payment method data
+#[instrument(skip_all, fields(flow = ?Flow::GetExtendedCardInfo, payment_id))]
+pub async fn retrieve_extended_card_info(
+    state: web::Data<app::AppState>,
+    req: actix_web::HttpRequest,
+    path: web::Path<String>,
+) -> impl Responder {
+    let flow = Flow::GetExtendedCardInfo;
+    let payment_id = path.into_inner();
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        payment_id,
+        |state, auth, payment_id, _| {
+            payments::get_extended_card_info(state, auth.merchant_account.merchant_id, payment_id)
+        },
+        &auth::ApiKeyAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 pub fn get_or_generate_payment_id(
     payload: &mut payment_types::PaymentsRequest,
 ) -> errors::RouterResult<()> {
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 705596abe84..719fa84a999 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -408,6 +408,8 @@ pub enum Flow {
     RetrievePollStatus,
     /// Toggles the extended card info feature in profile level
     ToggleExtendedCardInfo,
+    /// Get the extended card info associated to a payment_id
+    GetExtendedCardInfo,
 }
 
 ///
diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json
index bb8945c530a..d0addf07c98 100644
--- a/openapi/openapi_spec.json
+++ b/openapi/openapi_spec.json
@@ -8707,6 +8707,17 @@
           "mandate_revoked"
         ]
       },
+      "ExtendedCardInfoResponse": {
+        "type": "object",
+        "required": [
+          "payload"
+        ],
+        "properties": {
+          "payload": {
+            "type": "string"
+          }
+        }
+      },
       "ExternalAuthenticationDetailsResponse": {
         "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 -->
This PR adds a new endpoint to retrieve the extended card info (encrypted) stored in redis under a given merchant and payment. 

### Additional Changes

- [x] 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)?
-->
This PR cannot be tested until https://github.com/juspay/hyperswitch/issues/4482 goes in.
For local testing I manually set redis entry and tried to hit this newly added endpoint.
1. Create merchant account and api key
2. Create a payment
3. Got the payment id and merchant id and set the redis entry 
```
set merchant_1713971568_pay_1nrIoi2hRnzsy1V2cEv6_extended_card_info "eyJlbmMiOiJBMjU2R0NNIiwidHlwIjoiSldUIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.sTmCdgRVEz4hZKZ5VxQxq4Dt_V6v9O_96dIXy5F6lzdePOVQ2CwYJQeMhrsFjfk8kZrdOsfeOfFV1FVmxhWjREUvVBZKld5p47HDi4GAEDCX10HH9hIE3SD-IfU4D9riyE2MpIiWoB6xMVNxOvZSWAh2r_-UBgMzAoTAvz7ecmFJopRa8691up8H-Bc7DsVUxfAnEPef9bUIXd2Ej8vWR_XNyx9giPPwhYqYwtcsyyBnpnkyJW26Cc0J-zgbkT-6SPzTfpt_ALUons8nj3uQkmurpaQGdbBj9Zb_BOjK7ytngohnr68Z6ftb0-K41XYMrl11v1Rb-S_enQBstMsyVA.kh4VJ86tKDm5mwh9.WzF74jEuhZMQrYvNgl3e8Ll8YVMXwOizKdgbdBF0i6VWywXKPCIlwD150gtKo15u1_Xvo-151jTAp56E1w3vT2vOUyXWf96MTFytZR3VE4ClLB27AIu_4oUPJ5iG9IVLcMsUxEp4-OM0kNtrpbUY-Fyb3COwZ5AtZ2Fhx_CSiUp1ZdyvoP7XEfkwWgSGU2qW1s9P5VxsaKyFtva7nPo2trJw8-805P3Qtdm-27XvFYNKcTRYUJ9XBDn-cinbo-qrqe2XFQ5lh7fClhHK2H_hiJdk_WBq4iZKBK-XJpeDjCCNtiAVDGw.e2duyijt4Py7nmU-1PygSg"
```
4. Hit the endpoint to retrieve the payload
```
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)

5. If another merchant tries to retrieve the same payload by passing same payment_id, throw 404
![image](https://github.com/juspay/hyperswitch/assets/70657455/048e2d6b-2c70-4bd8-94e2-ccafd97e3ab2)

## 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
