Bug: Create a merchant config for enable `pg_agnostic_mit`



Create a merchant config (`pg_agnostic_mit`) to enable usage of Network Transaction/Reference ID in recurring mandate payments. This will be a `business profile` level config that a merchant specifies.

**API Spec:-**
```
POST /business_profile/{bp_id}/configs/pg_agnostic_mit
{
    “enabled”: true,
}
```
diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs
index 4d32f1d0f81..723e6eccc36 100644
--- a/crates/api_models/src/routing.rs
+++ b/crates/api_models/src/routing.rs
@@ -11,7 +11,7 @@ pub use euclid::{
 use serde::{Deserialize, Serialize};
 use utoipa::ToSchema;
 
-use crate::enums::{self, RoutableConnectors, TransactionType};
+use crate::enums::{RoutableConnectors, TransactionType};
 
 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
 #[serde(tag = "type", content = "data", rename_all = "snake_case")]
@@ -299,35 +299,13 @@ impl From<RoutableConnectorChoice> for ast::ConnectorChoice {
     }
 }
 
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
 pub struct DetailedConnectorChoice {
-    pub connector: RoutableConnectors,
-    pub business_label: Option<String>,
-    pub business_country: Option<enums::CountryAlpha2>,
-    pub business_sub_label: Option<String>,
-}
-
-impl DetailedConnectorChoice {
-    pub fn get_connector_label(&self) -> Option<String> {
-        self.business_country
-            .as_ref()
-            .zip(self.business_label.as_ref())
-            .map(|(business_country, business_label)| {
-                let mut base_label = format!(
-                    "{}_{:?}_{}",
-                    self.connector, business_country, business_label
-                );
-
-                if let Some(ref sub_label) = self.business_sub_label {
-                    base_label.push('_');
-                    base_label.push_str(sub_label);
-                }
-
-                base_label
-            })
-    }
+    pub enabled: bool,
 }
 
+impl common_utils::events::ApiEventMetric for DetailedConnectorChoice {}
+
 #[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, strum::Display, ToSchema)]
 #[serde(rename_all = "snake_case")]
 #[strum(serialize_all = "snake_case")]
diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs
index 31825617398..bec90c51e9c 100644
--- a/crates/router/src/core/routing.rs
+++ b/crates/router/src/core/routing.rs
@@ -9,7 +9,6 @@ use api_models::{
 };
 #[cfg(not(feature = "business_profile_routing"))]
 use common_utils::ext_traits::{Encode, StringExt};
-#[cfg(not(feature = "business_profile_routing"))]
 use diesel_models::configs;
 #[cfg(feature = "business_profile_routing")]
 use diesel_models::routing_algorithm::RoutingAlgorithm;
@@ -807,6 +806,37 @@ pub async fn retrieve_linked_routing_config(
     }
 }
 
+pub async fn upsert_connector_agnostic_mandate_config(
+    state: AppState,
+    business_profile_id: &str,
+    mandate_config: routing_types::DetailedConnectorChoice,
+) -> RouterResponse<routing_types::DetailedConnectorChoice> {
+    let key = helpers::get_pg_agnostic_mandate_config_key(business_profile_id);
+
+    let mandate_config_str = mandate_config.enabled.to_string();
+
+    let find_config = state
+        .store
+        .find_config_by_key_unwrap_or(&key, Some(mandate_config_str.clone()))
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("error saving pg agnostic mandate config to db")?;
+
+    if find_config.config != mandate_config_str {
+        let config_update = configs::ConfigUpdate::Update {
+            config: Some(mandate_config_str),
+        };
+        state
+            .store
+            .update_config_by_key(&key, config_update)
+            .await
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("error saving pg agnostic mandate config to db")?;
+    }
+
+    Ok(service_api::ApplicationResponse::Json(mandate_config))
+}
+
 pub async fn retrieve_default_routing_config_for_profiles(
     state: AppState,
     merchant_account: domain::MerchantAccount,
diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs
index febd1cd86dd..a725cc714e2 100644
--- a/crates/router/src/core/routing/helpers.rs
+++ b/crates/router/src/core/routing/helpers.rs
@@ -273,9 +273,9 @@ pub async fn update_business_profile_active_algorithm_ref(
 
 pub async fn get_merchant_connector_agnostic_mandate_config(
     db: &dyn StorageInterface,
-    merchant_id: &str,
+    business_profile_id: &str,
 ) -> RouterResult<Vec<routing_types::DetailedConnectorChoice>> {
-    let key = get_pg_agnostic_mandate_config_key(merchant_id);
+    let key = get_pg_agnostic_mandate_config_key(business_profile_id);
     let maybe_config = db.find_config_by_key(&key).await;
 
     match maybe_config {
@@ -312,29 +312,6 @@ pub async fn get_merchant_connector_agnostic_mandate_config(
     }
 }
 
-pub async fn update_merchant_connector_agnostic_mandate_config(
-    db: &dyn StorageInterface,
-    merchant_id: &str,
-    mandate_config: Vec<routing_types::DetailedConnectorChoice>,
-) -> RouterResult<Vec<routing_types::DetailedConnectorChoice>> {
-    let key = get_pg_agnostic_mandate_config_key(merchant_id);
-    let mandate_config_str = mandate_config
-        .encode_to_string_of_json()
-        .change_context(errors::ApiErrorResponse::InternalServerError)
-        .attach_printable("unable to serialize pg agnostic mandate config during update")?;
-
-    let config_update = configs::ConfigUpdate::Update {
-        config: Some(mandate_config_str),
-    };
-
-    db.update_config_by_key(&key, config_update)
-        .await
-        .change_context(errors::ApiErrorResponse::InternalServerError)
-        .attach_printable("error saving pg agnostic mandate config to db")?;
-
-    Ok(mandate_config)
-}
-
 pub async fn validate_connectors_in_routing_config(
     db: &dyn StorageInterface,
     key_store: &domain::MerchantKeyStore,
@@ -465,8 +442,8 @@ pub fn get_routing_dictionary_key(merchant_id: &str) -> String {
 
 /// Provides the identifier for the specific merchant's agnostic_mandate_config
 #[inline(always)]
-pub fn get_pg_agnostic_mandate_config_key(merchant_id: &str) -> String {
-    format!("pg_agnostic_mandate_{merchant_id}")
+pub fn get_pg_agnostic_mandate_config_key(business_profile_id: &str) -> String {
+    format!("pg_agnostic_mandate_{business_profile_id}")
 }
 
 /// Provides the identifier for the specific merchant's default_config
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 01ed994a9d3..7d9ee1670dc 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -437,6 +437,10 @@ impl Routing {
                         )
                     })),
             )
+            .service(
+                web::resource("/business_profile/{business_profile_id}/configs/pg_agnostic_mit")
+                    .route(web::post().to(cloud_routing::upsert_connector_agnostic_mandate_config)),
+            )
             .service(
                 web::resource("/default")
                     .route(web::get().to(|state, req| {
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 7908b9dfdca..02a3a6841f4 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -225,6 +225,7 @@ impl From<Flow> for ApiIdentifier {
             | Flow::ReconTokenRequest
             | Flow::ReconServiceRequest
             | Flow::ReconVerifyToken => Self::Recon,
+            Flow::CreateConnectorAgnosticMandateConfig => Self::Routing,
         }
     }
 }
diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs
index 1476fc7b3d6..28d65f66a82 100644
--- a/crates/router/src/routes/routing.rs
+++ b/crates/router/src/routes/routing.rs
@@ -566,6 +566,44 @@ pub async fn routing_retrieve_linked_config(
     }
 }
 
+#[cfg(feature = "olap")]
+#[instrument(skip_all)]
+pub async fn upsert_connector_agnostic_mandate_config(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<routing_types::DetailedConnectorChoice>,
+    path: web::Path<String>,
+) -> impl Responder {
+    use crate::services::authentication::AuthenticationData;
+
+    let flow = Flow::CreateConnectorAgnosticMandateConfig;
+    let business_profile_id = path.into_inner();
+
+    Box::pin(oss_api::server_wrap(
+        flow,
+        state,
+        &req,
+        json_payload.into_inner(),
+        |state, _auth: AuthenticationData, mandate_config| {
+            Box::pin(routing::upsert_connector_agnostic_mandate_config(
+                state,
+                &business_profile_id,
+                mandate_config,
+            ))
+        },
+        #[cfg(not(feature = "release"))]
+        auth::auth_type(
+            &auth::ApiKeyAuth,
+            &auth::JWTAuth(Permission::RoutingWrite),
+            req.headers(),
+        ),
+        #[cfg(feature = "release")]
+        &auth::JWTAuth(Permission::RoutingWrite),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 #[cfg(feature = "olap")]
 #[instrument(skip_all)]
 pub async fn routing_retrieve_default_config_for_profiles(
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index df322da9429..ca8d75b3324 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -206,6 +206,8 @@ pub enum Flow {
     RoutingRetrieveConfig,
     /// Routing retrieve active config
     RoutingRetrieveActiveConfig,
+    /// Update connector agnostic mandate config
+    CreateConnectorAgnosticMandateConfig,
     /// Routing retrieve default config
     RoutingRetrieveDefaultConfig,
     /// Routing retrieve dictionary

## 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 merchant config (`pg_agnostic_mit`) to enable usage of Network Transaction/Reference ID in recurring `MIT` payments. This will be a `business profile` level config that a merchant specifies.

### 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)?
-->
Set processor agnostic MIT config as `true`
<img width="737" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/76c54b6a-888c-42fc-b5b2-024eecabf868">
config status from db
![image](https://github.com/juspay/hyperswitch/assets/83439957/dbcd0961-685d-4e76-92da-48ed7da33756)
Set processor agnostic MIT config as `false`
<img width="654" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/e0dbd263-bc26-4167-ae96-f6a65cd0730c">
config status from db
![image](https://github.com/juspay/hyperswitch/assets/83439957/632144b1-8c0f-42c6-9232-8f2c4571ba3e)

```curl
curl --location 'https://sandbox.hyperswitch.io/routing/business_profile/:business_profile_id/configs/pg_agnostic_mit' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer JWT_Token' \
--data '{
    "enabled": true
}'
```

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