Bug: System config for enabling this feature for a specific merchant (by business profile)



Adding a feature flag at business profile level to enable raw card forwarding. 
Feature flag to be set using /profile/:id:/extended_card_info with auth as admin api key
diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs
index 88d584b6a71..cfc9694dda3 100644
--- a/crates/api_models/src/admin.rs
+++ b/crates/api_models/src/admin.rs
@@ -1088,3 +1088,10 @@ pub struct PaymentLinkConfig {
     /// Enable saved payment method option for payment link
     pub enabled_saved_payment_method: bool,
 }
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
+pub struct ExtendedCardInfoChoice {
+    pub enabled: bool,
+}
+
+impl common_utils::events::ApiEventMetric for ExtendedCardInfoChoice {}
diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs
index d5d4f5d805f..7911aa403d6 100644
--- a/crates/diesel_models/src/business_profile.rs
+++ b/crates/diesel_models/src/business_profile.rs
@@ -35,6 +35,7 @@ pub struct BusinessProfile {
     pub payment_link_config: Option<serde_json::Value>,
     pub session_expiry: Option<i64>,
     pub authentication_connector_details: Option<serde_json::Value>,
+    pub is_extended_card_info_enabled: Option<bool>,
 }
 
 #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
@@ -61,6 +62,7 @@ pub struct BusinessProfileNew {
     pub payment_link_config: Option<serde_json::Value>,
     pub session_expiry: Option<i64>,
     pub authentication_connector_details: Option<serde_json::Value>,
+    pub is_extended_card_info_enabled: Option<bool>,
 }
 
 #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)]
@@ -84,6 +86,84 @@ pub struct BusinessProfileUpdateInternal {
     pub payment_link_config: Option<serde_json::Value>,
     pub session_expiry: Option<i64>,
     pub authentication_connector_details: Option<serde_json::Value>,
+    pub is_extended_card_info_enabled: Option<bool>,
+}
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub enum BusinessProfileUpdate {
+    Update {
+        profile_name: Option<String>,
+        modified_at: Option<time::PrimitiveDateTime>,
+        return_url: Option<String>,
+        enable_payment_response_hash: Option<bool>,
+        payment_response_hash_key: Option<String>,
+        redirect_to_merchant_with_http_post: Option<bool>,
+        webhook_details: Option<serde_json::Value>,
+        metadata: Option<pii::SecretSerdeValue>,
+        routing_algorithm: Option<serde_json::Value>,
+        intent_fulfillment_time: Option<i64>,
+        frm_routing_algorithm: Option<serde_json::Value>,
+        payout_routing_algorithm: Option<serde_json::Value>,
+        is_recon_enabled: Option<bool>,
+        applepay_verified_domains: Option<Vec<String>>,
+        payment_link_config: Option<serde_json::Value>,
+        session_expiry: Option<i64>,
+        authentication_connector_details: Option<serde_json::Value>,
+    },
+    ExtendedCardInfoUpdate {
+        is_extended_card_info_enabled: Option<bool>,
+    },
+}
+
+impl From<BusinessProfileUpdate> for BusinessProfileUpdateInternal {
+    fn from(business_profile_update: BusinessProfileUpdate) -> Self {
+        match business_profile_update {
+            BusinessProfileUpdate::Update {
+                profile_name,
+                modified_at,
+                return_url,
+                enable_payment_response_hash,
+                payment_response_hash_key,
+                redirect_to_merchant_with_http_post,
+                webhook_details,
+                metadata,
+                routing_algorithm,
+                intent_fulfillment_time,
+                frm_routing_algorithm,
+                payout_routing_algorithm,
+                is_recon_enabled,
+                applepay_verified_domains,
+                payment_link_config,
+                session_expiry,
+                authentication_connector_details,
+            } => Self {
+                profile_name,
+                modified_at,
+                return_url,
+                enable_payment_response_hash,
+                payment_response_hash_key,
+                redirect_to_merchant_with_http_post,
+                webhook_details,
+                metadata,
+                routing_algorithm,
+                intent_fulfillment_time,
+                frm_routing_algorithm,
+                payout_routing_algorithm,
+                is_recon_enabled,
+                applepay_verified_domains,
+                payment_link_config,
+                session_expiry,
+                authentication_connector_details,
+                ..Default::default()
+            },
+            BusinessProfileUpdate::ExtendedCardInfoUpdate {
+                is_extended_card_info_enabled,
+            } => Self {
+                is_extended_card_info_enabled,
+                ..Default::default()
+            },
+        }
+    }
 }
 
 impl From<BusinessProfileNew> for BusinessProfile {
@@ -109,13 +189,14 @@ impl From<BusinessProfileNew> for BusinessProfile {
             payment_link_config: new.payment_link_config,
             session_expiry: new.session_expiry,
             authentication_connector_details: new.authentication_connector_details,
+            is_extended_card_info_enabled: new.is_extended_card_info_enabled,
         }
     }
 }
 
-impl BusinessProfileUpdateInternal {
+impl BusinessProfileUpdate {
     pub fn apply_changeset(self, source: BusinessProfile) -> BusinessProfile {
-        let Self {
+        let BusinessProfileUpdateInternal {
             profile_name,
             modified_at: _,
             return_url,
@@ -133,7 +214,8 @@ impl BusinessProfileUpdateInternal {
             payment_link_config,
             session_expiry,
             authentication_connector_details,
-        } = self;
+            is_extended_card_info_enabled,
+        } = self.into();
         BusinessProfile {
             profile_name: profile_name.unwrap_or(source.profile_name),
             modified_at: common_utils::date_time::now(),
@@ -154,6 +236,7 @@ impl BusinessProfileUpdateInternal {
             payment_link_config,
             session_expiry,
             authentication_connector_details,
+            is_extended_card_info_enabled,
             ..source
         }
     }
diff --git a/crates/diesel_models/src/query/business_profile.rs b/crates/diesel_models/src/query/business_profile.rs
index effe2219a69..609a5c55f24 100644
--- a/crates/diesel_models/src/query/business_profile.rs
+++ b/crates/diesel_models/src/query/business_profile.rs
@@ -2,7 +2,9 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, T
 
 use super::generics;
 use crate::{
-    business_profile::{BusinessProfile, BusinessProfileNew, BusinessProfileUpdateInternal},
+    business_profile::{
+        BusinessProfile, BusinessProfileNew, BusinessProfileUpdate, BusinessProfileUpdateInternal,
+    },
     errors,
     schema::business_profile::dsl,
     PgPooledConn, StorageResult,
@@ -18,12 +20,12 @@ impl BusinessProfile {
     pub async fn update_by_profile_id(
         self,
         conn: &PgPooledConn,
-        business_profile: BusinessProfileUpdateInternal,
+        business_profile: BusinessProfileUpdate,
     ) -> StorageResult<Self> {
         match generics::generic_update_by_id::<<Self as HasTable>::Table, _, _, _>(
             conn,
             self.profile_id.clone(),
-            business_profile,
+            BusinessProfileUpdateInternal::from(business_profile),
         )
         .await
         {
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index bbe8a8060fc..44429cc4cad 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -193,6 +193,7 @@ diesel::table! {
         payment_link_config -> Nullable<Jsonb>,
         session_expiry -> Nullable<Int8>,
         authentication_connector_details -> Nullable<Jsonb>,
+        is_extended_card_info_enabled -> Nullable<Bool>,
     }
 }
 
diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs
index dcb5dc14fee..3c6ab1f31d6 100644
--- a/crates/router/src/core/admin.rs
+++ b/crates/router/src/core/admin.rs
@@ -1648,7 +1648,7 @@ pub async fn update_business_profile(
         })
         .transpose()?;
 
-    let business_profile_update = storage::business_profile::BusinessProfileUpdateInternal {
+    let business_profile_update = storage::business_profile::BusinessProfileUpdate::Update {
         profile_name: request.profile_name,
         modified_at: Some(date_time::now()),
         return_url: request.return_url.map(|return_url| return_url.to_string()),
@@ -1691,6 +1691,39 @@ pub async fn update_business_profile(
     ))
 }
 
+pub async fn extended_card_info_toggle(
+    state: AppState,
+    profile_id: &str,
+    ext_card_info_choice: admin_types::ExtendedCardInfoChoice,
+) -> RouterResponse<admin_types::ExtendedCardInfoChoice> {
+    let db = state.store.as_ref();
+    let business_profile = db
+        .find_business_profile_by_profile_id(profile_id)
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
+            id: profile_id.to_string(),
+        })?;
+
+    if business_profile.is_extended_card_info_enabled.is_none()
+        || business_profile
+            .is_extended_card_info_enabled
+            .is_some_and(|existing_config| existing_config != ext_card_info_choice.enabled)
+    {
+        let business_profile_update =
+            storage::business_profile::BusinessProfileUpdate::ExtendedCardInfoUpdate {
+                is_extended_card_info_enabled: Some(ext_card_info_choice.enabled),
+            };
+
+        db.update_business_profile_by_profile_id(business_profile, business_profile_update)
+            .await
+            .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
+                id: profile_id.to_owned(),
+            })?;
+    }
+
+    Ok(service_api::ApplicationResponse::Json(ext_card_info_choice))
+}
+
 pub(crate) fn validate_auth_and_metadata_type(
     connector_name: api_models::enums::Connector,
     val: &types::ConnectorAuthType,
diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs
index a725cc714e2..62925dcc2a5 100644
--- a/crates/router/src/core/routing/helpers.rs
+++ b/crates/router/src/core/routing/helpers.rs
@@ -5,7 +5,7 @@
 use api_models::routing as routing_types;
 use common_utils::ext_traits::Encode;
 use diesel_models::{
-    business_profile::{BusinessProfile, BusinessProfileUpdateInternal},
+    business_profile::{BusinessProfile, BusinessProfileUpdate},
     configs,
 };
 use error_stack::ResultExt;
@@ -245,7 +245,7 @@ pub async fn update_business_profile_active_algorithm_ref(
         storage::enums::TransactionType::Payout => (None, Some(ref_val)),
     };
 
-    let business_profile_update = BusinessProfileUpdateInternal {
+    let business_profile_update = BusinessProfileUpdate::Update {
         profile_name: None,
         return_url: None,
         enable_payment_response_hash: None,
diff --git a/crates/router/src/db/business_profile.rs b/crates/router/src/db/business_profile.rs
index 4d5a94b370a..7b5ade8c965 100644
--- a/crates/router/src/db/business_profile.rs
+++ b/crates/router/src/db/business_profile.rs
@@ -30,7 +30,7 @@ pub trait BusinessProfileInterface {
     async fn update_business_profile_by_profile_id(
         &self,
         current_state: business_profile::BusinessProfile,
-        business_profile_update: business_profile::BusinessProfileUpdateInternal,
+        business_profile_update: business_profile::BusinessProfileUpdate,
     ) -> CustomResult<business_profile::BusinessProfile, errors::StorageError>;
 
     async fn delete_business_profile_by_profile_id_merchant_id(
@@ -90,7 +90,7 @@ impl BusinessProfileInterface for Store {
     async fn update_business_profile_by_profile_id(
         &self,
         current_state: business_profile::BusinessProfile,
-        business_profile_update: business_profile::BusinessProfileUpdateInternal,
+        business_profile_update: business_profile::BusinessProfileUpdate,
     ) -> CustomResult<business_profile::BusinessProfile, errors::StorageError> {
         let conn = connection::pg_connection_write(self).await?;
         storage::business_profile::BusinessProfile::update_by_profile_id(
@@ -169,7 +169,7 @@ impl BusinessProfileInterface for MockDb {
     async fn update_business_profile_by_profile_id(
         &self,
         current_state: business_profile::BusinessProfile,
-        business_profile_update: business_profile::BusinessProfileUpdateInternal,
+        business_profile_update: business_profile::BusinessProfileUpdate,
     ) -> CustomResult<business_profile::BusinessProfile, errors::StorageError> {
         self.business_profiles
             .lock()
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 9787e366e79..cc9c98be833 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -2039,7 +2039,7 @@ impl BusinessProfileInterface for KafkaStore {
     async fn update_business_profile_by_profile_id(
         &self,
         current_state: business_profile::BusinessProfile,
-        business_profile_update: business_profile::BusinessProfileUpdateInternal,
+        business_profile_update: business_profile::BusinessProfileUpdate,
     ) -> CustomResult<business_profile::BusinessProfile, errors::StorageError> {
         self.diesel_store
             .update_business_profile_by_profile_id(current_state, business_profile_update)
diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs
index bc534dde390..63d9f840a00 100644
--- a/crates/router/src/routes/admin.rs
+++ b/crates/router/src/routes/admin.rs
@@ -612,3 +612,25 @@ pub async fn merchant_account_kv_status(
     )
     .await
 }
+
+#[instrument(skip_all, fields(flow = ?Flow::ToggleExtendedCardInfo))]
+pub async fn toggle_extended_card_info(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<(String, String)>,
+    json_payload: web::Json<api_models::admin::ExtendedCardInfoChoice>,
+) -> HttpResponse {
+    let flow = Flow::ToggleExtendedCardInfo;
+    let (_, profile_id) = path.into_inner();
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        json_payload.into_inner(),
+        |state, _, req, _| extended_card_info_toggle(state, &profile_id, req),
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index ff660dbf7b7..a7c6f1486d3 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1104,10 +1104,17 @@ impl BusinessProfile {
                     .route(web::get().to(business_profiles_list)),
             )
             .service(
-                web::resource("/{profile_id}")
-                    .route(web::get().to(business_profile_retrieve))
-                    .route(web::post().to(business_profile_update))
-                    .route(web::delete().to(business_profile_delete)),
+                web::scope("/{profile_id}")
+                    .service(
+                        web::resource("")
+                            .route(web::get().to(business_profile_retrieve))
+                            .route(web::post().to(business_profile_update))
+                            .route(web::delete().to(business_profile_delete)),
+                    )
+                    .service(
+                        web::resource("/toggle_extended_card_info")
+                            .route(web::post().to(toggle_extended_card_info)),
+                    ),
             )
     }
 }
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index aec3a5f2c1f..c7b1b28f2eb 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -166,7 +166,8 @@ impl From<Flow> for ApiIdentifier {
             | Flow::BusinessProfileUpdate
             | Flow::BusinessProfileRetrieve
             | Flow::BusinessProfileDelete
-            | Flow::BusinessProfileList => Self::Business,
+            | Flow::BusinessProfileList
+            | Flow::ToggleExtendedCardInfo => Self::Business,
 
             Flow::PaymentLinkRetrieve
             | Flow::PaymentLinkInitiate
diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs
index 2cc0e21d64f..06935a29fb3 100644
--- a/crates/router/src/types/api/admin.rs
+++ b/crates/router/src/types/api/admin.rs
@@ -175,6 +175,7 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)>
                 .change_context(errors::ApiErrorResponse::InvalidDataValue {
                     field_name: "authentication_connector_details",
                 })?,
+            is_extended_card_info_enabled: None,
         })
     }
 }
diff --git a/crates/router/src/types/storage/business_profile.rs b/crates/router/src/types/storage/business_profile.rs
index 2ab7597bcda..d7c7d66b99e 100644
--- a/crates/router/src/types/storage/business_profile.rs
+++ b/crates/router/src/types/storage/business_profile.rs
@@ -1,3 +1,3 @@
 pub use diesel_models::business_profile::{
-    BusinessProfile, BusinessProfileNew, BusinessProfileUpdateInternal,
+    BusinessProfile, BusinessProfileNew, BusinessProfileUpdate, BusinessProfileUpdateInternal,
 };
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 0ec6894debe..705596abe84 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -406,6 +406,8 @@ pub enum Flow {
     WebhookEventDeliveryRetry,
     /// Retrieve status of the Poll
     RetrievePollStatus,
+    /// Toggles the extended card info feature in profile level
+    ToggleExtendedCardInfo,
 }
 
 ///
diff --git a/migrations/2024-04-23-132120_add-extended-card-info-to-business-profile/down.sql b/migrations/2024-04-23-132120_add-extended-card-info-to-business-profile/down.sql
new file mode 100644
index 00000000000..e503c2dfbea
--- /dev/null
+++ b/migrations/2024-04-23-132120_add-extended-card-info-to-business-profile/down.sql
@@ -0,0 +1,3 @@
+-- This file should undo anything in `up.sql`
+
+ALTER TABLE business_profile DROP COLUMN IF EXISTS is_extended_card_info_enabled;
\ No newline at end of file
diff --git a/migrations/2024-04-23-132120_add-extended-card-info-to-business-profile/up.sql b/migrations/2024-04-23-132120_add-extended-card-info-to-business-profile/up.sql
new file mode 100644
index 00000000000..cd5e8851adf
--- /dev/null
+++ b/migrations/2024-04-23-132120_add-extended-card-info-to-business-profile/up.sql
@@ -0,0 +1,3 @@
+-- Your SQL goes here
+
+ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS is_extended_card_info_enabled BOOLEAN DEFAULT FALSE;
\ No newline at end of file

## 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 an api under `business_profile` domain, to toggle the `extended_card_info` feature with `admin_api_key` as auth.
This PR also includes refactoring of `business_profile` updation approach.

### Additional Changes

- [ ] This PR modifies the API contract
- [x] 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 a business profile for a merchant (Default one which gets created during merchant account creation works too)
2. Toggle 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)

3. For verifying, u could check the db if the feature is toggled using below query
```
SELECT is_extended_card_info_enabled FROM business_profile WHERE profile_id = 'profile_id';
```

## 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
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
