Bug: [FEATURE] Recon APIs



### Feature Description

Add recon related APIs in HyperSwitch for managing recon module for the consumers. Recon dashboard is a separate application hosted at https://sandbox.hyperswitch.io/recon-dashboard/

Recon module in HyperSwitch should be able to perform below tasks
- Toggle recon module for a given merchant
- Generate tokens for Recon dashboard login
- Let admins modify the state of recon related stuff for a given merchant
- Verify the generated token

### Possible Implementation

Below endpoints are exposed for performing the required tasks

- `/recon/update_merchant` - admin API call for updating any recon related field for a given merchant.
- `/recon/token` - request a token for an user which is used for logging into Recon's dashboard.
- `/recon/request` - let's user send a mail to HS team for enabling recon for their merchant's account.
- `/recon/verify_token` - validate a generated token.


### Have you spent some time checking if this feature request has been raised before?

- [X] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!
diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml
index 69980361500..45702a4ecb0 100644
--- a/crates/api_models/Cargo.toml
+++ b/crates/api_models/Cargo.toml
@@ -8,7 +8,7 @@ readme = "README.md"
 license.workspace = true
 
 [features]
-default = ["payouts", "frm"]
+default = ["payouts", "frm", "recon"]
 business_profile_routing = []
 connector_choice_bcompat = []
 errors = ["dep:actix-web", "dep:reqwest"]
@@ -18,6 +18,7 @@ dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"]
 detailed_errors = []
 payouts = []
 frm = []
+recon = []
 
 [dependencies]
 actix-web = { version = "4.3.1", optional = true }
diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs
index 6d9bd5db342..26a9d222d6b 100644
--- a/crates/api_models/src/events.rs
+++ b/crates/api_models/src/events.rs
@@ -5,6 +5,8 @@ mod locker_migration;
 pub mod payment;
 #[cfg(feature = "payouts")]
 pub mod payouts;
+#[cfg(feature = "recon")]
+pub mod recon;
 pub mod refund;
 pub mod routing;
 pub mod user;
diff --git a/crates/api_models/src/events/recon.rs b/crates/api_models/src/events/recon.rs
new file mode 100644
index 00000000000..aed648f4c86
--- /dev/null
+++ b/crates/api_models/src/events/recon.rs
@@ -0,0 +1,21 @@
+use common_utils::events::{ApiEventMetric, ApiEventsType};
+
+use crate::recon::{ReconStatusResponse, ReconTokenResponse, ReconUpdateMerchantRequest};
+
+impl ApiEventMetric for ReconUpdateMerchantRequest {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Recon)
+    }
+}
+
+impl ApiEventMetric for ReconTokenResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Recon)
+    }
+}
+
+impl ApiEventMetric for ReconStatusResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Recon)
+    }
+}
diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index 1f4cb7359c7..c0743c8b8fc 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -1,7 +1,11 @@
 use common_utils::events::{ApiEventMetric, ApiEventsType};
+#[cfg(feature = "recon")]
+use masking::PeekInterface;
 
 #[cfg(feature = "dummy_connector")]
 use crate::user::sample_data::SampleDataRequest;
+#[cfg(feature = "recon")]
+use crate::user::VerifyTokenResponse;
 use crate::user::{
     dashboard_metadata::{
         GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
@@ -21,6 +25,16 @@ impl ApiEventMetric for DashboardEntryResponse {
     }
 }
 
+#[cfg(feature = "recon")]
+impl ApiEventMetric for VerifyTokenResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::User {
+            merchant_id: self.merchant_id.clone(),
+            user_id: self.user_email.peek().to_string(),
+        })
+    }
+}
+
 common_utils::impl_misc_api_event_type!(
     SignUpRequest,
     SignUpWithMerchantIdRequest,
diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs
index dc1f6eb6537..1ea79ff6fe8 100644
--- a/crates/api_models/src/lib.rs
+++ b/crates/api_models/src/lib.rs
@@ -26,6 +26,8 @@ pub mod payments;
 #[cfg(feature = "payouts")]
 pub mod payouts;
 pub mod pm_auth;
+#[cfg(feature = "recon")]
+pub mod recon;
 pub mod refunds;
 pub mod routing;
 pub mod surcharge_decision_configs;
diff --git a/crates/api_models/src/recon.rs b/crates/api_models/src/recon.rs
new file mode 100644
index 00000000000..efbe28f96ba
--- /dev/null
+++ b/crates/api_models/src/recon.rs
@@ -0,0 +1,21 @@
+use common_utils::pii;
+use masking::Secret;
+
+use crate::enums;
+
+#[derive(serde::Deserialize, Debug, serde::Serialize)]
+pub struct ReconUpdateMerchantRequest {
+    pub merchant_id: String,
+    pub recon_status: enums::ReconStatus,
+    pub user_email: pii::Email,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct ReconTokenResponse {
+    pub token: Secret<String>,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct ReconStatusResponse {
+    pub recon_status: enums::ReconStatus,
+}
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index f5af31c8e7f..a04c4fef660 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -140,3 +140,10 @@ pub struct UserMerchantAccount {
     pub merchant_id: String,
     pub merchant_name: OptionalEncryptableName,
 }
+
+#[cfg(feature = "recon")]
+#[derive(serde::Serialize, Debug)]
+pub struct VerifyTokenResponse {
+    pub merchant_id: String,
+    pub user_email: pii::Email,
+}
diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs
index 6bbf78afe42..c2bf50d96c3 100644
--- a/crates/common_utils/src/events.rs
+++ b/crates/common_utils/src/events.rs
@@ -49,6 +49,7 @@ pub enum ApiEventsType {
     Miscellaneous,
     RustLocker,
     FraudCheck,
+    Recon,
 }
 
 impl ApiEventMetric for serde_json::Value {}
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index 8ecac362091..0a544e0bd09 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -9,7 +9,7 @@ readme = "README.md"
 license.workspace = true
 
 [features]
-default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"]
+default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm", "recon"]
 s3 = ["dep:aws-sdk-s3", "dep:aws-config"]
 kms = ["external_services/kms", "dep:aws-config"]
 email = ["external_services/email", "dep:aws-config", "olap"]
@@ -30,6 +30,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect
 external_access_dc = ["dummy_connector"]
 detailed_errors = ["api_models/detailed_errors", "error-stack/serde"]
 payouts = []
+recon = ["email"]
 retry = []
 
 [dependencies]
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index b1a582cedec..27a4f67618e 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -757,3 +757,32 @@ pub async fn send_verification_mail(
 
     Ok(ApplicationResponse::StatusOk)
 }
+
+#[cfg(feature = "recon")]
+pub async fn verify_token(
+    state: AppState,
+    req: auth::ReconUser,
+) -> UserResponse<user_api::VerifyTokenResponse> {
+    let user = state
+        .store
+        .find_user_by_id(&req.user_id)
+        .await
+        .map_err(|e| {
+            if e.current_context().is_db_not_found() {
+                e.change_context(UserErrors::UserNotFound)
+            } else {
+                e.change_context(UserErrors::InternalServerError)
+            }
+        })?;
+    let merchant_id = state
+        .store
+        .find_user_role_by_user_id(&req.user_id)
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .merchant_id;
+
+    Ok(ApplicationResponse::Json(user_api::VerifyTokenResponse {
+        merchant_id: merchant_id.to_string(),
+        user_email: user.email,
+    }))
+}
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index 696198f2153..c38a4dc85b5 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -165,6 +165,12 @@ pub fn mk_app(
     {
         server_app = server_app.service(routes::StripeApis::server(state.clone()));
     }
+
+    #[cfg(feature = "recon")]
+    {
+        server_app = server_app.service(routes::Recon::server(state.clone()));
+    }
+
     server_app = server_app.service(routes::Cards::server(state.clone()));
     server_app = server_app.service(routes::Cache::server(state.clone()));
     server_app = server_app.service(routes::Health::server(state));
diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs
index d4bfabb6f92..d9916f98e74 100644
--- a/crates/router/src/routes.rs
+++ b/crates/router/src/routes.rs
@@ -28,6 +28,8 @@ pub mod payment_methods;
 pub mod payments;
 #[cfg(feature = "payouts")]
 pub mod payouts;
+#[cfg(feature = "recon")]
+pub mod recon;
 pub mod refunds;
 #[cfg(feature = "olap")]
 pub mod routing;
@@ -53,6 +55,8 @@ pub use self::app::DummyConnector;
 pub use self::app::Forex;
 #[cfg(feature = "payouts")]
 pub use self::app::Payouts;
+#[cfg(all(feature = "olap", feature = "recon"))]
+pub use self::app::Recon;
 #[cfg(all(feature = "olap", feature = "kms"))]
 pub use self::app::Verify;
 pub use self::app::{
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 77253d1d75c..0c489dbe63a 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -40,6 +40,8 @@ use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*};
 use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
 #[cfg(all(feature = "frm", feature = "oltp"))]
 use crate::routes::fraud_check as frm_routes;
+#[cfg(all(feature = "recon", feature = "olap"))]
+use crate::routes::recon as recon_routes;
 #[cfg(feature = "olap")]
 use crate::routes::verify_connector::payment_connector_verify;
 pub use crate::{
@@ -568,6 +570,26 @@ impl PaymentMethods {
     }
 }
 
+#[cfg(all(feature = "olap", feature = "recon"))]
+pub struct Recon;
+
+#[cfg(all(feature = "olap", feature = "recon"))]
+impl Recon {
+    pub fn server(state: AppState) -> Scope {
+        web::scope("/recon")
+            .app_data(web::Data::new(state))
+            .service(
+                web::resource("/update_merchant")
+                    .route(web::post().to(recon_routes::update_merchant)),
+            )
+            .service(web::resource("/token").route(web::get().to(recon_routes::get_recon_token)))
+            .service(
+                web::resource("/request").route(web::post().to(recon_routes::request_for_recon)),
+            )
+            .service(web::resource("/verify_token").route(web::get().to(verify_recon_token)))
+    }
+}
+
 #[cfg(feature = "olap")]
 pub struct Blocklist;
 
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index c560f0d988a..12cf76be475 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -31,6 +31,7 @@ pub enum ApiIdentifier {
     User,
     UserRole,
     ConnectorOnboarding,
+    Recon,
 }
 
 impl From<Flow> for ApiIdentifier {
@@ -186,6 +187,11 @@ impl From<Flow> for ApiIdentifier {
             Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
                 Self::ConnectorOnboarding
             }
+
+            Flow::ReconMerchantUpdate
+            | Flow::ReconTokenRequest
+            | Flow::ReconServiceRequest
+            | Flow::ReconVerifyToken => Self::Recon,
         }
     }
 }
diff --git a/crates/router/src/routes/recon.rs b/crates/router/src/routes/recon.rs
new file mode 100644
index 00000000000..d34e30237dd
--- /dev/null
+++ b/crates/router/src/routes/recon.rs
@@ -0,0 +1,250 @@
+use actix_web::{web, HttpRequest, HttpResponse};
+use api_models::recon as recon_api;
+use common_enums::ReconStatus;
+use error_stack::ResultExt;
+use masking::{ExposeInterface, PeekInterface, Secret};
+use router_env::Flow;
+
+use super::AppState;
+use crate::{
+    core::{
+        api_locking,
+        errors::{self, RouterResponse, RouterResult, StorageErrorExt, UserErrors},
+    },
+    services::{
+        api as service_api, api,
+        authentication::{self as auth, ReconUser, UserFromToken},
+        email::types as email_types,
+        recon::ReconToken,
+    },
+    types::{
+        api::{self as api_types, enums},
+        domain::{UserEmail, UserFromStorage, UserName},
+        storage,
+    },
+};
+
+pub async fn update_merchant(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<recon_api::ReconUpdateMerchantRequest>,
+) -> HttpResponse {
+    let flow = Flow::ReconMerchantUpdate;
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        json_payload.into_inner(),
+        |state, _user, req| recon_merchant_account_update(state, req),
+        &auth::ReconAdmin,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn request_for_recon(state: web::Data<AppState>, http_req: HttpRequest) -> HttpResponse {
+    let flow = Flow::ReconServiceRequest;
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &http_req,
+        (),
+        |state, user: UserFromToken, _req| send_recon_request(state, user),
+        &auth::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn get_recon_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
+    let flow = Flow::ReconTokenRequest;
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        (),
+        |state, user: ReconUser, _| generate_recon_token(state, user),
+        &auth::ReconJWT,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn send_recon_request(
+    state: AppState,
+    user: UserFromToken,
+) -> RouterResponse<recon_api::ReconStatusResponse> {
+    let db = &*state.store;
+    let user_from_db = db
+        .find_user_by_id(&user.user_id)
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)?;
+    let merchant_id = db
+        .find_user_role_by_user_id(&user.user_id)
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?
+        .merchant_id;
+    let key_store = db
+        .get_merchant_key_store_by_merchant_id(
+            merchant_id.as_str(),
+            &db.get_master_key().to_vec().into(),
+        )
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
+    let merchant_account = db
+        .find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store)
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
+
+    let email_contents = email_types::ProFeatureRequest {
+        feature_name: "RECONCILIATION & SETTLEMENT".to_string(),
+        merchant_id: merchant_id.clone(),
+        user_name: UserName::new(user_from_db.name)
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to form username")?,
+        recipient_email: UserEmail::from_pii_email(user_from_db.email.clone())
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to convert to UserEmail from pii::Email")?,
+        settings: state.conf.clone(),
+        subject: format!(
+            "Dashboard Pro Feature Request by {}",
+            user_from_db.email.expose().peek()
+        ),
+    };
+
+    let is_email_sent = state
+        .email_client
+        .compose_and_send_email(
+            Box::new(email_contents),
+            state.conf.proxy.https_url.as_ref(),
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to compose and send email for ProFeatureRequest")
+        .is_ok();
+
+    if is_email_sent {
+        let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
+            recon_status: enums::ReconStatus::Requested,
+        };
+
+        let response = db
+            .update_merchant(merchant_account, updated_merchant_account, &key_store)
+            .await
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable_lazy(|| {
+                format!("Failed while updating merchant's recon status: {merchant_id}")
+            })?;
+
+        Ok(service_api::ApplicationResponse::Json(
+            recon_api::ReconStatusResponse {
+                recon_status: response.recon_status,
+            },
+        ))
+    } else {
+        Ok(service_api::ApplicationResponse::Json(
+            recon_api::ReconStatusResponse {
+                recon_status: enums::ReconStatus::NotRequested,
+            },
+        ))
+    }
+}
+
+pub async fn recon_merchant_account_update(
+    state: AppState,
+    req: recon_api::ReconUpdateMerchantRequest,
+) -> RouterResponse<api_types::MerchantAccountResponse> {
+    let merchant_id = &req.merchant_id.clone();
+    let user_email = &req.user_email.clone();
+
+    let db = &*state.store;
+
+    let key_store = db
+        .get_merchant_key_store_by_merchant_id(
+            &req.merchant_id,
+            &db.get_master_key().to_vec().into(),
+        )
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
+
+    let merchant_account = db
+        .find_merchant_account_by_merchant_id(merchant_id, &key_store)
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
+
+    let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
+        recon_status: req.recon_status,
+    };
+
+    let response = db
+        .update_merchant(merchant_account, updated_merchant_account, &key_store)
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable_lazy(|| {
+            format!("Failed while updating merchant's recon status: {merchant_id}")
+        })?;
+
+    let email_contents = email_types::ReconActivation {
+        recipient_email: UserEmail::from_pii_email(user_email.clone())
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to convert to UserEmail from pii::Email")?,
+        user_name: UserName::new(Secret::new("HyperSwitch User".to_string()))
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to form username")?,
+        settings: state.conf.clone(),
+        subject: "Approval of Recon Request - Access Granted to Recon Dashboard",
+    };
+
+    if req.recon_status == ReconStatus::Active {
+        let _is_email_sent = state
+            .email_client
+            .compose_and_send_email(
+                Box::new(email_contents),
+                state.conf.proxy.https_url.as_ref(),
+            )
+            .await
+            .change_context(UserErrors::InternalServerError)
+            .attach_printable("Failed to compose and send email for ReconActivation")
+            .is_ok();
+    }
+
+    Ok(service_api::ApplicationResponse::Json(
+        response
+            .try_into()
+            .change_context(errors::ApiErrorResponse::InvalidDataValue {
+                field_name: "merchant_account",
+            })?,
+    ))
+}
+
+pub async fn generate_recon_token(
+    state: AppState,
+    req: ReconUser,
+) -> RouterResponse<recon_api::ReconTokenResponse> {
+    let db = &*state.store;
+    let user = db
+        .find_user_by_id(&req.user_id)
+        .await
+        .map_err(|e| {
+            if e.current_context().is_db_not_found() {
+                e.change_context(errors::ApiErrorResponse::InvalidJwtToken)
+            } else {
+                e.change_context(errors::ApiErrorResponse::InternalServerError)
+            }
+        })?
+        .into();
+
+    let token = Box::pin(get_recon_auth_token(user, state))
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)?;
+    Ok(service_api::ApplicationResponse::Json(
+        recon_api::ReconTokenResponse { token },
+    ))
+}
+
+pub async fn get_recon_auth_token(
+    user: UserFromStorage,
+    state: AppState,
+) -> RouterResult<Secret<String>> {
+    ReconToken::new_token(user.0.user_id.clone(), &state.conf).await
+}
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index a77b82c550e..976fd5c9f56 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -388,3 +388,18 @@ pub async fn verify_email_request(
     ))
     .await
 }
+
+#[cfg(feature = "recon")]
+pub async fn verify_recon_token(state: web::Data<AppState>, http_req: HttpRequest) -> HttpResponse {
+    let flow = Flow::ReconVerifyToken;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &http_req,
+        (),
+        |state, user, _req| user_core::verify_token(state, user),
+        &auth::ReconJWT,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs
index 57f3b802bd5..8c973105d53 100644
--- a/crates/router/src/services.rs
+++ b/crates/router/src/services.rs
@@ -7,6 +7,8 @@ pub mod jwt;
 pub mod kafka;
 pub mod logger;
 pub mod pm_auth;
+#[cfg(feature = "recon")]
+pub mod recon;
 
 #[cfg(feature = "email")]
 pub mod email;
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index b48465ebd17..3370912394e 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -12,10 +12,14 @@ use serde::Serialize;
 use super::authorization::{self, permissions::Permission};
 #[cfg(feature = "olap")]
 use super::jwt;
+#[cfg(feature = "recon")]
+use super::recon::ReconToken;
 #[cfg(feature = "olap")]
 use crate::consts;
 #[cfg(feature = "olap")]
 use crate::core::errors::UserResult;
+#[cfg(feature = "recon")]
+use crate::routes::AppState;
 use crate::{
     configs::settings,
     core::{
@@ -822,3 +826,95 @@ where
     }
     default_auth
 }
+
+#[cfg(feature = "recon")]
+static RECON_API_KEY: tokio::sync::OnceCell<StrongSecret<String>> =
+    tokio::sync::OnceCell::const_new();
+
+#[cfg(feature = "recon")]
+pub async fn get_recon_admin_api_key(
+    secrets: &settings::Secrets,
+    #[cfg(feature = "kms")] kms_client: &kms::KmsClient,
+) -> RouterResult<&'static StrongSecret<String>> {
+    RECON_API_KEY
+        .get_or_try_init(|| async {
+            #[cfg(feature = "kms")]
+            let recon_admin_api_key = secrets
+                .kms_encrypted_recon_admin_api_key
+                .decrypt_inner(kms_client)
+                .await
+                .change_context(errors::ApiErrorResponse::InternalServerError)
+                .attach_printable("Failed to KMS decrypt recon admin API key")?;
+
+            #[cfg(not(feature = "kms"))]
+            let recon_admin_api_key = secrets.recon_admin_api_key.clone();
+
+            Ok(StrongSecret::new(recon_admin_api_key))
+        })
+        .await
+}
+
+#[cfg(feature = "recon")]
+pub struct ReconAdmin;
+
+#[async_trait]
+#[cfg(feature = "recon")]
+impl<A> AuthenticateAndFetch<(), A> for ReconAdmin
+where
+    A: AppStateInfo + Sync,
+{
+    async fn authenticate_and_fetch(
+        &self,
+        request_headers: &HeaderMap,
+        state: &A,
+    ) -> RouterResult<((), AuthenticationType)> {
+        let request_admin_api_key =
+            get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
+        let conf = state.conf();
+
+        let admin_api_key = get_recon_admin_api_key(
+            &conf.secrets,
+            #[cfg(feature = "kms")]
+            kms::get_kms_client(&conf.kms).await,
+        )
+        .await?;
+
+        if request_admin_api_key != admin_api_key.peek() {
+            Err(report!(errors::ApiErrorResponse::Unauthorized)
+                .attach_printable("Recon Admin Authentication Failure"))?;
+        }
+
+        Ok(((), AuthenticationType::NoAuth))
+    }
+}
+
+#[cfg(feature = "recon")]
+pub struct ReconJWT;
+#[cfg(feature = "recon")]
+pub struct ReconUser {
+    pub user_id: String,
+}
+#[cfg(feature = "recon")]
+impl AuthInfo for ReconUser {
+    fn get_merchant_id(&self) -> Option<&str> {
+        None
+    }
+}
+#[cfg(all(feature = "olap", feature = "recon"))]
+#[async_trait]
+impl AuthenticateAndFetch<ReconUser, AppState> for ReconJWT {
+    async fn authenticate_and_fetch(
+        &self,
+        request_headers: &HeaderMap,
+        state: &AppState,
+    ) -> RouterResult<(ReconUser, AuthenticationType)> {
+        let payload = parse_jwt_payload::<AppState, ReconToken>(request_headers, state).await?;
+
+        Ok((
+            ReconUser {
+                user_id: payload.user_id,
+            },
+            AuthenticationType::NoAuth,
+        ))
+    }
+}
diff --git a/crates/router/src/services/email/assets/recon_activated.html b/crates/router/src/services/email/assets/recon_activation.html
similarity index 100%
rename from crates/router/src/services/email/assets/recon_activated.html
rename to crates/router/src/services/email/assets/recon_activation.html
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index d5c28b1fd6a..0ef15eaa40d 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -1,17 +1,37 @@
 use common_utils::errors::CustomResult;
 use error_stack::ResultExt;
 use external_services::email::{EmailContents, EmailData, EmailError};
-use masking::ExposeInterface;
+use masking::{ExposeInterface, PeekInterface};
 
 use crate::{configs, consts};
 #[cfg(feature = "olap")]
 use crate::{core::errors::UserErrors, services::jwt, types::domain};
 
 pub enum EmailBody {
-    Verify { link: String },
-    Reset { link: String, user_name: String },
-    MagicLink { link: String, user_name: String },
-    InviteUser { link: String, user_name: String },
+    Verify {
+        link: String,
+    },
+    Reset {
+        link: String,
+        user_name: String,
+    },
+    MagicLink {
+        link: String,
+        user_name: String,
+    },
+    InviteUser {
+        link: String,
+        user_name: String,
+    },
+    ReconActivation {
+        user_name: String,
+    },
+    ProFeatureRequest {
+        feature_name: String,
+        merchant_id: String,
+        user_name: String,
+        user_email: String,
+    },
 }
 
 pub mod html {
@@ -43,6 +63,30 @@ pub mod html {
                     link = link
                 )
             }
+            EmailBody::ReconActivation { user_name } => {
+                format!(
+                    include_str!("assets/recon_activation.html"),
+                    username = user_name,
+                )
+            }
+            EmailBody::ProFeatureRequest {
+                feature_name,
+                merchant_id,
+                user_name,
+                user_email,
+            } => {
+                format!(
+                    "Dear Hyperswitch Support Team,
+    
+    Dashboard Pro Feature Request,
+    Feature name  : {feature_name}
+    Merchant ID   : {merchant_id}
+    Merchant Name : {user_name}
+    Email         : {user_email}
+    
+    (note: This is an auto generated email. use merchant email for any further comunications)",
+                )
+            }
         }
     }
 }
@@ -198,3 +242,54 @@ impl EmailData for InviteUser {
         })
     }
 }
+
+pub struct ReconActivation {
+    pub recipient_email: domain::UserEmail,
+    pub user_name: domain::UserName,
+    pub settings: std::sync::Arc<configs::settings::Settings>,
+    pub subject: &'static str,
+}
+
+#[async_trait::async_trait]
+impl EmailData for ReconActivation {
+    async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
+        let body = html::get_html_body(EmailBody::ReconActivation {
+            user_name: self.user_name.clone().get_secret().expose(),
+        });
+
+        Ok(EmailContents {
+            subject: self.subject.to_string(),
+            body: external_services::email::IntermediateString::new(body),
+            recipient: self.recipient_email.clone().into_inner(),
+        })
+    }
+}
+
+pub struct ProFeatureRequest {
+    pub recipient_email: domain::UserEmail,
+    pub feature_name: String,
+    pub merchant_id: String,
+    pub user_name: domain::UserName,
+    pub settings: std::sync::Arc<configs::settings::Settings>,
+    pub subject: String,
+}
+
+#[async_trait::async_trait]
+impl EmailData for ProFeatureRequest {
+    async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
+        let recipient = self.recipient_email.clone().into_inner();
+
+        let body = html::get_html_body(EmailBody::ProFeatureRequest {
+            user_name: self.user_name.clone().get_secret().expose(),
+            feature_name: self.feature_name.clone(),
+            merchant_id: self.merchant_id.clone(),
+            user_email: recipient.peek().to_string(),
+        });
+
+        Ok(EmailContents {
+            subject: self.subject.clone(),
+            body: external_services::email::IntermediateString::new(body),
+            recipient,
+        })
+    }
+}
diff --git a/crates/router/src/services/recon.rs b/crates/router/src/services/recon.rs
new file mode 100644
index 00000000000..d5a2151a487
--- /dev/null
+++ b/crates/router/src/services/recon.rs
@@ -0,0 +1,29 @@
+use error_stack::ResultExt;
+use masking::Secret;
+
+use super::jwt;
+use crate::{
+    consts,
+    core::{self, errors::RouterResult},
+    routes::app::settings::Settings,
+};
+
+#[derive(serde::Serialize, serde::Deserialize)]
+pub struct ReconToken {
+    pub user_id: String,
+    pub exp: u64,
+}
+
+impl ReconToken {
+    pub async fn new_token(user_id: String, settings: &Settings) -> RouterResult<Secret<String>> {
+        let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS);
+        let exp = jwt::generate_exp(exp_duration)
+            .change_context(core::errors::ApiErrorResponse::InternalServerError)?
+            .as_secs();
+        let token_payload = Self { user_id, exp };
+        let token = jwt::generate_jwt(&token_payload, settings)
+            .await
+            .change_context(core::errors::ApiErrorResponse::InternalServerError)?;
+        Ok(Secret::new(token))
+    }
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 8f0b9bad3e8..0d6636e567d 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -165,6 +165,14 @@ pub enum Flow {
     RefundsList,
     // Retrieve forex flow.
     RetrieveForexFlow,
+    /// Toggles recon service for a merchant.
+    ReconMerchantUpdate,
+    /// Recon token request flow.
+    ReconTokenRequest,
+    /// Initial request for recon service.
+    ReconServiceRequest,
+    /// Recon token verification flow
+    ReconVerifyToken,
     /// Routing create flow,
     RoutingCreateConfig,
     /// Routing link config

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

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

## Description
This PR adds recon APIs for activating recon as a feature on HyperSwitch dashboard and generating tokens for Recon dashboard.

Issue - #3359 

### 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?
Tested below endpoints for Recon service using the attached postman collection.
Postman collection - https://galactic-capsule-229427.postman.co/workspace/My-Workspace~2b563e0d-bad3-420f-8c0b-0fd5b278a4fe/collection/9906252-4ef01bc4-53d4-40c0-a3e7-c84c8e32f105?action=share&creator=9906252

**Endpoints**
1. `/recon/update_merchant` - admin API call for updating any recon related field for a given merchant.
2. `/recon/token` - request a token for an user which is used for logging into Recon's dashboard. 
3. `/recon/request` - let's user send a mail to HS team for enabling recon for their merchant's account.
4. `/recon/verify_token` - validate a generated token. 

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