Bug: feat(user): list apis



Create new apis to list for user:
- orgs
- merchant accounts
- profiles.

diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 03e59db1e86..9867aba7b39 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -390,3 +390,21 @@ pub struct UserKeyTransferRequest {
 pub struct UserTransferKeyResponse {
     pub total_transferred: usize,
 }
+
+#[derive(Debug, serde::Serialize)]
+pub struct ListOrgsForUserResponse {
+    pub org_id: id_type::OrganizationId,
+    pub org_name: Option<String>,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct ListMerchantsForUserInOrgResponse {
+    pub merchant_id: id_type::MerchantId,
+    pub merchant_name: OptionalEncryptableName,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct ListProfilesForUserInOrgAndMerchantAccountResponse {
+    pub profile_id: String,
+    pub profile_name: String,
+}
diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs
index b7f79a4438f..b4789b3c169 100644
--- a/crates/diesel_models/src/query/user_role.rs
+++ b/crates/diesel_models/src/query/user_role.rs
@@ -1,9 +1,14 @@
+use async_bb8_diesel::AsyncRunQueryDsl;
 use common_utils::id_type;
-use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods};
+use diesel::{
+    associations::HasTable, debug_query, pg::Pg, result::Error as DieselError,
+    BoolExpressionMethods, ExpressionMethods, QueryDsl,
+};
+use error_stack::{report, ResultExt};
 
 use crate::{
-    enums::UserRoleVersion, query::generics, schema::user_roles::dsl, user_role::*, PgPooledConn,
-    StorageResult,
+    enums::UserRoleVersion, errors, query::generics, schema::user_roles::dsl, user_role::*,
+    PgPooledConn, StorageResult,
 };
 
 impl UserRoleNew {
@@ -179,4 +184,55 @@ impl UserRole {
         generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(conn, predicate)
             .await
     }
+
+    pub async fn generic_user_roles_list(
+        conn: &PgPooledConn,
+        user_id: String,
+        org_id: Option<id_type::OrganizationId>,
+        merchant_id: Option<id_type::MerchantId>,
+        profile_id: Option<String>,
+        entity_id: Option<String>,
+        version: Option<UserRoleVersion>,
+    ) -> StorageResult<Vec<Self>> {
+        let mut query = <Self as HasTable>::table()
+            .filter(dsl::user_id.eq(user_id))
+            .into_boxed();
+
+        if let Some(org_id) = org_id {
+            query = query.filter(dsl::org_id.eq(org_id));
+        }
+
+        if let Some(merchant_id) = merchant_id {
+            query = query.filter(dsl::merchant_id.eq(merchant_id));
+        }
+
+        if let Some(profile_id) = profile_id {
+            query = query.filter(dsl::profile_id.eq(profile_id));
+        }
+
+        if let Some(entity_id) = entity_id {
+            query = query.filter(dsl::entity_id.eq(entity_id));
+        }
+
+        if let Some(version) = version {
+            query = query.filter(dsl::version.eq(version));
+        }
+
+        router_env::logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
+
+        match generics::db_metrics::track_database_call::<Self, _, _>(
+            query.get_results_async(conn),
+            generics::db_metrics::DatabaseOperation::Filter,
+        )
+        .await
+        {
+            Ok(value) => Ok(value),
+            Err(err) => match err {
+                DieselError::NotFound => {
+                    Err(report!(err)).change_context(errors::DatabaseError::NotFound)
+                }
+                _ => Err(report!(err)).change_context(errors::DatabaseError::Others),
+            },
+        }
+    }
 }
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 671d3ee1535..c3d0dbae04e 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -1,14 +1,16 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 
 use api_models::{
     payments::RedirectionResponse,
     user::{self as user_api, InviteMultipleUserResponse},
 };
+use common_enums::EntityType;
 use common_utils::{type_name, types::keymanager::Identifier};
 #[cfg(feature = "email")]
 use diesel_models::user_role::UserRoleUpdate;
 use diesel_models::{
     enums::{TotpStatus, UserRoleVersion, UserStatus},
+    organization::OrganizationBridge,
     user as storage_user,
     user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate},
     user_role::UserRoleNew,
@@ -2563,3 +2565,198 @@ pub async fn terminate_auth_select(
         token,
     )
 }
+
+pub async fn list_orgs_for_user(
+    state: SessionState,
+    user_from_token: auth::UserFromToken,
+) -> UserResponse<Vec<user_api::ListOrgsForUserResponse>> {
+    let orgs = state
+        .store
+        .list_user_roles(
+            user_from_token.user_id.as_str(),
+            None,
+            None,
+            None,
+            None,
+            None,
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into_iter()
+        .filter_map(|user_role| {
+            (user_role.status == UserStatus::Active)
+                .then_some(user_role.org_id)
+                .flatten()
+        })
+        .collect::<HashSet<_>>();
+
+    let resp = futures::future::try_join_all(
+        orgs.iter()
+            .map(|org_id| state.store.find_organization_by_org_id(org_id)),
+    )
+    .await
+    .change_context(UserErrors::InternalServerError)?
+    .into_iter()
+    .map(|org| user_api::ListOrgsForUserResponse {
+        org_id: org.get_organization_id(),
+        org_name: org.get_organization_name(),
+    })
+    .collect();
+
+    Ok(ApplicationResponse::Json(resp))
+}
+
+pub async fn list_merchants_for_user_in_org(
+    state: SessionState,
+    user_from_token: auth::UserFromToken,
+) -> UserResponse<Vec<user_api::ListMerchantsForUserInOrgResponse>> {
+    let role_info = roles::RoleInfo::from_role_id(
+        &state,
+        &user_from_token.role_id,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .change_context(UserErrors::InternalServerError)?;
+    let merchant_accounts = if role_info.get_entity_type() == EntityType::Organization {
+        state
+            .store
+            .list_merchant_accounts_by_organization_id(
+                &(&state).into(),
+                user_from_token.org_id.get_string_repr(),
+            )
+            .await
+            .change_context(UserErrors::InternalServerError)?
+            .into_iter()
+            .map(
+                |merchant_account| user_api::ListMerchantsForUserInOrgResponse {
+                    merchant_name: merchant_account.merchant_name.clone(),
+                    merchant_id: merchant_account.get_id().to_owned(),
+                },
+            )
+            .collect()
+    } else {
+        let merchant_ids = state
+            .store
+            .list_user_roles(
+                user_from_token.user_id.as_str(),
+                Some(&user_from_token.org_id),
+                None,
+                None,
+                None,
+                None,
+            )
+            .await
+            .change_context(UserErrors::InternalServerError)?
+            .into_iter()
+            .filter_map(|user_role| {
+                (user_role.status == UserStatus::Active)
+                    .then_some(user_role.merchant_id)
+                    .flatten()
+            })
+            .collect::<HashSet<_>>()
+            .into_iter()
+            .collect();
+        state
+            .store
+            .list_multiple_merchant_accounts(&(&state).into(), merchant_ids)
+            .await
+            .change_context(UserErrors::InternalServerError)?
+            .into_iter()
+            .map(
+                |merchant_account| user_api::ListMerchantsForUserInOrgResponse {
+                    merchant_name: merchant_account.merchant_name.clone(),
+                    merchant_id: merchant_account.get_id().to_owned(),
+                },
+            )
+            .collect()
+    };
+
+    Ok(ApplicationResponse::Json(merchant_accounts))
+}
+
+pub async fn list_profiles_for_user_in_org_and_merchant_account(
+    state: SessionState,
+    user_from_token: auth::UserFromToken,
+) -> UserResponse<Vec<user_api::ListProfilesForUserInOrgAndMerchantAccountResponse>> {
+    let role_info = roles::RoleInfo::from_role_id(
+        &state,
+        &user_from_token.role_id,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .change_context(UserErrors::InternalServerError)?;
+
+    let key_manager_state = &(&state).into();
+    let key_store = state
+        .store
+        .get_merchant_key_store_by_merchant_id(
+            key_manager_state,
+            &user_from_token.merchant_id,
+            &state.store.get_master_key().to_vec().into(),
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)?;
+    let user_role_level = role_info.get_entity_type();
+    let profiles =
+        if user_role_level == EntityType::Organization || user_role_level == EntityType::Merchant {
+            state
+                .store
+                .list_business_profile_by_merchant_id(
+                    key_manager_state,
+                    &key_store,
+                    &user_from_token.merchant_id,
+                )
+                .await
+                .change_context(UserErrors::InternalServerError)?
+                .into_iter()
+                .map(
+                    |profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse {
+                        profile_id: profile.profile_id,
+                        profile_name: profile.profile_name,
+                    },
+                )
+                .collect()
+        } else {
+            let profile_ids = state
+                .store
+                .list_user_roles(
+                    user_from_token.user_id.as_str(),
+                    Some(&user_from_token.org_id),
+                    Some(&user_from_token.merchant_id),
+                    None,
+                    None,
+                    None,
+                )
+                .await
+                .change_context(UserErrors::InternalServerError)?
+                .into_iter()
+                .filter_map(|user_role| {
+                    (user_role.status == UserStatus::Active)
+                        .then_some(user_role.profile_id)
+                        .flatten()
+                })
+                .collect::<HashSet<_>>();
+
+            futures::future::try_join_all(profile_ids.iter().map(|profile_id| {
+                state.store.find_business_profile_by_profile_id(
+                    key_manager_state,
+                    &key_store,
+                    profile_id,
+                )
+            }))
+            .await
+            .change_context(UserErrors::InternalServerError)?
+            .into_iter()
+            .map(
+                |profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse {
+                    profile_id: profile.profile_id,
+                    profile_name: profile.profile_name,
+                },
+            )
+            .collect()
+        };
+
+    Ok(ApplicationResponse::Json(profiles))
+}
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index c22f327983a..cb3f6db144e 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -2871,6 +2871,20 @@ impl UserRoleInterface for KafkaStore {
             .list_user_roles_by_merchant_id(merchant_id, version)
             .await
     }
+
+    async fn list_user_roles(
+        &self,
+        user_id: &str,
+        org_id: Option<&id_type::OrganizationId>,
+        merchant_id: Option<&id_type::MerchantId>,
+        profile_id: Option<&String>,
+        entity_id: Option<&String>,
+        version: Option<enums::UserRoleVersion>,
+    ) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
+        self.diesel_store
+            .list_user_roles(user_id, org_id, merchant_id, profile_id, entity_id, version)
+            .await
+    }
 }
 
 #[async_trait::async_trait]
diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs
index a93aeb1ce34..f028437b346 100644
--- a/crates/router/src/db/user_role.rs
+++ b/crates/router/src/db/user_role.rs
@@ -69,6 +69,16 @@ pub trait UserRoleInterface {
         profile_id: Option<&String>,
         version: enums::UserRoleVersion,
     ) -> CustomResult<storage::UserRole, errors::StorageError>;
+
+    async fn list_user_roles(
+        &self,
+        user_id: &str,
+        org_id: Option<&id_type::OrganizationId>,
+        merchant_id: Option<&id_type::MerchantId>,
+        profile_id: Option<&String>,
+        entity_id: Option<&String>,
+        version: Option<enums::UserRoleVersion>,
+    ) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
 }
 
 #[async_trait::async_trait]
@@ -206,6 +216,29 @@ impl UserRoleInterface for Store {
         .await
         .map_err(|error| report!(errors::StorageError::from(error)))
     }
+
+    async fn list_user_roles(
+        &self,
+        user_id: &str,
+        org_id: Option<&id_type::OrganizationId>,
+        merchant_id: Option<&id_type::MerchantId>,
+        profile_id: Option<&String>,
+        entity_id: Option<&String>,
+        version: Option<enums::UserRoleVersion>,
+    ) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
+        let conn = connection::pg_connection_read(self).await?;
+        storage::UserRole::generic_user_roles_list(
+            &conn,
+            user_id.to_owned(),
+            org_id.cloned(),
+            merchant_id.cloned(),
+            profile_id.cloned(),
+            entity_id.cloned(),
+            version,
+        )
+        .await
+        .map_err(|error| report!(errors::StorageError::from(error)))
+    }
 }
 
 #[async_trait::async_trait]
@@ -471,4 +504,51 @@ impl UserRoleInterface for MockDb {
             .into()),
         }
     }
+
+    async fn list_user_roles(
+        &self,
+        user_id: &str,
+        org_id: Option<&id_type::OrganizationId>,
+        merchant_id: Option<&id_type::MerchantId>,
+        profile_id: Option<&String>,
+        entity_id: Option<&String>,
+        version: Option<enums::UserRoleVersion>,
+    ) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
+        let user_roles = self.user_roles.lock().await;
+
+        let filtered_roles: Vec<_> = user_roles
+            .iter()
+            .filter_map(|role| {
+                let mut filter_condition = role.user_id == user_id;
+
+                role.org_id
+                    .as_ref()
+                    .zip(org_id)
+                    .inspect(|(role_org_id, org_id)| {
+                        filter_condition = filter_condition && role_org_id == org_id
+                    });
+                role.merchant_id.as_ref().zip(merchant_id).inspect(
+                    |(role_merchant_id, merchant_id)| {
+                        filter_condition = filter_condition && role_merchant_id == merchant_id
+                    },
+                );
+                role.profile_id.as_ref().zip(profile_id).inspect(
+                    |(role_profile_id, profile_id)| {
+                        filter_condition = filter_condition && role_profile_id == profile_id
+                    },
+                );
+                role.entity_id
+                    .as_ref()
+                    .zip(entity_id)
+                    .inspect(|(role_entity_id, entity_id)| {
+                        filter_condition = filter_condition && role_entity_id == entity_id
+                    });
+                version.inspect(|ver| filter_condition = filter_condition && ver == &role.version);
+
+                filter_condition.then(|| role.to_owned())
+            })
+            .collect();
+
+        Ok(filtered_roles)
+    }
 }
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 25e13ba417e..3ec877bdd9f 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1632,6 +1632,18 @@ impl User {
                 .service(web::resource("/transfer").route(web::post().to(transfer_user_key))),
         );
 
+        route = route.service(
+            web::scope("/list")
+                .service(web::resource("/org").route(web::get().to(list_orgs_for_user)))
+                .service(
+                    web::resource("/merchant").route(web::get().to(list_merchants_for_user_in_org)),
+                )
+                .service(
+                    web::resource("/profile")
+                        .route(web::get().to(list_profiles_for_user_in_org_and_merchant)),
+                ),
+        );
+
         // Two factor auth routes
         route = route.service(
             web::scope("/2fa")
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index b08451a870e..e08b477d29a 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -244,6 +244,9 @@ impl From<Flow> for ApiIdentifier {
             | Flow::UserTransferKey
             | Flow::GetSsoAuthUrl
             | Flow::SignInWithSso
+            | Flow::ListOrgForUser
+            | Flow::ListMerchantsForUserInOrg
+            | Flow::ListProfileForUserInOrgAndMerchant
             | Flow::AuthSelect => Self::User,
 
             Flow::ListRoles
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 39839e423ea..39ec3a24ff2 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -914,3 +914,58 @@ pub async fn transfer_user_key(
     ))
     .await
 }
+
+pub async fn list_orgs_for_user(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
+    let flow = Flow::ListOrgForUser;
+
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        (),
+        |state, user_from_token, _, _| user_core::list_orgs_for_user(state, user_from_token),
+        &auth::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn list_merchants_for_user_in_org(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+) -> HttpResponse {
+    let flow = Flow::ListMerchantsForUserInOrg;
+
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        (),
+        |state, user_from_token, _, _| {
+            user_core::list_merchants_for_user_in_org(state, user_from_token)
+        },
+        &auth::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn list_profiles_for_user_in_org_and_merchant(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+) -> HttpResponse {
+    let flow = Flow::ListProfileForUserInOrgAndMerchant;
+
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        (),
+        |state, user_from_token, _, _| {
+            user_core::list_profiles_for_user_in_org_and_merchant_account(state, user_from_token)
+        },
+        &auth::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index d1e552275ce..4fbbfdf5518 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -456,6 +456,12 @@ pub enum Flow {
     SignInWithSso,
     /// Auth Select
     AuthSelect,
+    /// List Orgs for user
+    ListOrgForUser,
+    /// List Merchants for user in org
+    ListMerchantsForUserInOrg,
+    /// List Profile for user in org and merchant
+    ListProfileForUserInOrgAndMerchant,
     /// List initial webhook delivery attempts
     WebhookEventInitialDeliveryAttemptList,
     /// List delivery attempts for a webhook event

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

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

## Description
New APIs list:
-  orgs for a user.
- merchant accounts for a user in current organisation.
- profile id for a user in current merchant  account.
<!-- Describe your changes in detail -->


### 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
Need new list apis for dashboard frontend.
This would allow user to see the list of all the orgs, merchant accounts (in select org) & profile ids (in select merchant account).
<!--
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?
List orgs
```sh
curl --location 'localhost:8080/user/list/org' \
--header 'Authorization: Bearer <JWT>' \
```
response example
```json
[
    {
        "org_id": "org_t4ryeBxwt9RKu8jlsTWq",
        "org_name": "Juspay"
    },
     {
        "org_id": "org_t4ryeBxwt9RKu8jlsTWq2",
        "org_name": "Juspay2"
    }
]
```

List merchants
```sh
curl --location 'localhost:8080/user/list/merchant' \
--header 'Authorization: Bearer <JWT>' \
```
response example
```json
[
    {
        "merchant_id": "juspay",
        "merchant_name": "Juspay"
    },
    {
        "merchant_id": "merchant_1724247367",
        "merchant_name": "Juspay"
    }
]
```

List profile
```sh
curl --location 'localhost:8080/user/list/profile' \
--header 'Authorization: Bearer <JWT>' \
```

response example
```json
[
    {
        "profile_id": "pro_S2E1iXeSzluIrIqH7DNi",
        "profile_name": "default"
    },
    {
        "profile_id": "pro_S2E1iXeSzluIrIqH7DNi2",
        "profile_name": "profile2"
    }
]
```
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->


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