Bug: feat(users): Allow multiple org_admins in single org



Currently, there can only be a single org_admin in a org, and that is the person who signed up.

All the other users will be either merchant level or profile level.

Since, there can be a usecase where a org needs multiple org admins, we should allow multiple org_admins.
diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs
index 1cd25e54cb3..f4142842e21 100644
--- a/crates/diesel_models/src/query/user_role.rs
+++ b/crates/diesel_models/src/query/user_role.rs
@@ -5,7 +5,6 @@ use diesel::{
     BoolExpressionMethods, ExpressionMethods, QueryDsl,
 };
 use error_stack::{report, ResultExt};
-use router_env::logger;
 
 use crate::{
     enums::{UserRoleVersion, UserStatus},
@@ -23,21 +22,6 @@ impl UserRoleNew {
 }
 
 impl UserRole {
-    pub async fn insert_multiple_user_roles(
-        conn: &PgPooledConn,
-        user_roles: Vec<UserRoleNew>,
-    ) -> StorageResult<Vec<Self>> {
-        let query = diesel::insert_into(<Self>::table()).values(user_roles);
-
-        logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
-
-        query
-            .get_results_async(conn)
-            .await
-            .change_context(errors::DatabaseError::Others)
-            .attach_printable("Error while inserting user_roles")
-    }
-
     pub async fn find_by_user_id(
         conn: &PgPooledConn,
         user_id: String,
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index f36ecc18116..a1a78656de1 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -67,7 +67,6 @@ pub async fn signup_with_merchant_id(
             state.clone(),
             common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
             UserStatus::Active,
-            None,
         )
         .await?;
 
@@ -146,7 +145,6 @@ pub async fn signup_token_only_flow(
             state.clone(),
             common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
             UserStatus::Active,
-            None,
         )
         .await?;
 
@@ -247,7 +245,6 @@ pub async fn connect_account(
                 state.clone(),
                 common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
                 UserStatus::Active,
-                None,
             )
             .await?;
 
@@ -657,7 +654,7 @@ async fn handle_existing_user_invitation(
                     org_id: user_from_token.org_id.clone(),
                     merchant_id: user_from_token.merchant_id.clone(),
                 })
-                .insert_in_v1_and_v2(state)
+                .insert_in_v2(state)
                 .await?
         }
         EntityType::Profile => {
@@ -767,14 +764,21 @@ async fn handle_new_user_invitation(
     };
 
     let _user_role = match role_info.get_entity_type() {
-        EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+        EntityType::Organization => {
+            user_role
+                .add_entity(domain::OrganizationLevel {
+                    org_id: user_from_token.org_id.clone(),
+                })
+                .insert_in_v2(state)
+                .await?
+        }
         EntityType::Merchant => {
             user_role
                 .add_entity(domain::MerchantLevel {
                     org_id: user_from_token.org_id.clone(),
                     merchant_id: user_from_token.merchant_id.clone(),
                 })
-                .insert_in_v1_and_v2(state)
+                .insert_in_v2(state)
                 .await?
         }
         EntityType::Profile => {
@@ -1128,7 +1132,7 @@ pub async fn create_internal_user(
             org_id: internal_merchant.organization_id,
             merchant_id: internal_merchant_id,
         })
-        .insert_in_v1_and_v2(&state)
+        .insert_in_v2(&state)
         .await
         .change_context(UserErrors::InternalServerError)?;
 
@@ -1257,28 +1261,11 @@ pub async fn create_merchant_account(
 ) -> UserResponse<()> {
     let user_from_db = user_from_token.get_user_from_db(&state).await?;
 
-    let new_user = domain::NewUser::try_from((user_from_db, req, user_from_token))?;
-    let new_merchant = new_user.get_new_merchant();
+    let new_merchant = domain::NewUserMerchant::try_from((user_from_db, req, user_from_token))?;
     new_merchant
         .create_new_merchant_and_insert_in_db(state.to_owned())
         .await?;
 
-    let role_insertion_res = new_user
-        .insert_org_level_user_role_in_db(
-            state.clone(),
-            common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
-            UserStatus::Active,
-            Some(UserRoleVersion::V1),
-        )
-        .await;
-    if let Err(e) = role_insertion_res {
-        let _ = state
-            .store
-            .delete_merchant_account_by_merchant_id(&new_merchant.get_merchant_id())
-            .await;
-        return Err(e);
-    }
-
     Ok(ApplicationResponse::StatusOk)
 }
 
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index de10b70f507..11082a65a49 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -37,10 +37,7 @@ use super::{
     user::{sample_data::BatchSampleDataInterface, UserInterface},
     user_authentication_method::UserAuthenticationMethodInterface,
     user_key_store::UserKeyStoreInterface,
-    user_role::{
-        InsertUserRolePayload, ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload,
-        UserRoleInterface,
-    },
+    user_role::{ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload, UserRoleInterface},
 };
 #[cfg(feature = "payouts")]
 use crate::services::kafka::payout::KafkaPayout;
@@ -3018,8 +3015,8 @@ impl RedisConnInterface for KafkaStore {
 impl UserRoleInterface for KafkaStore {
     async fn insert_user_role(
         &self,
-        user_role: InsertUserRolePayload,
-    ) -> CustomResult<Vec<user_storage::UserRole>, errors::StorageError> {
+        user_role: storage::UserRoleNew,
+    ) -> CustomResult<user_storage::UserRole, errors::StorageError> {
         self.diesel_store.insert_user_role(user_role).await
     }
 
diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs
index d511010e6b5..e9e7cafa3fd 100644
--- a/crates/router/src/db/user_role.rs
+++ b/crates/router/src/db/user_role.rs
@@ -13,21 +13,6 @@ use crate::{
     services::Store,
 };
 
-pub enum InsertUserRolePayload {
-    OnlyV1(storage::UserRoleNew),
-    OnlyV2(storage::UserRoleNew),
-    V1AndV2(Box<[storage::UserRoleNew; 2]>),
-}
-
-impl InsertUserRolePayload {
-    fn convert_to_vec(self) -> Vec<storage::UserRoleNew> {
-        match self {
-            Self::OnlyV1(user_role) | Self::OnlyV2(user_role) => vec![user_role],
-            Self::V1AndV2(user_roles) => user_roles.to_vec(),
-        }
-    }
-}
-
 pub struct ListUserRolesByOrgIdPayload<'a> {
     pub user_id: Option<&'a String>,
     pub org_id: &'a id_type::OrganizationId,
@@ -51,8 +36,8 @@ pub struct ListUserRolesByUserIdPayload<'a> {
 pub trait UserRoleInterface {
     async fn insert_user_role(
         &self,
-        user_role: InsertUserRolePayload,
-    ) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
+        user_role: storage::UserRoleNew,
+    ) -> CustomResult<storage::UserRole, errors::StorageError>;
 
     async fn find_user_role_by_user_id(
         &self,
@@ -123,11 +108,12 @@ impl UserRoleInterface for Store {
     #[instrument(skip_all)]
     async fn insert_user_role(
         &self,
-        user_role: InsertUserRolePayload,
-    ) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
+        user_role: storage::UserRoleNew,
+    ) -> CustomResult<storage::UserRole, errors::StorageError> {
         let conn = connection::pg_connection_write(self).await?;
 
-        storage::UserRole::insert_multiple_user_roles(&conn, user_role.convert_to_vec())
+        user_role
+            .insert(&conn)
             .await
             .map_err(|error| report!(errors::StorageError::from(error)))
     }
@@ -295,44 +281,38 @@ impl UserRoleInterface for Store {
 impl UserRoleInterface for MockDb {
     async fn insert_user_role(
         &self,
-        user_role: InsertUserRolePayload,
-    ) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
+        user_role: storage::UserRoleNew,
+    ) -> CustomResult<storage::UserRole, errors::StorageError> {
         let mut db_user_roles = self.user_roles.lock().await;
 
-        user_role
-            .convert_to_vec()
-            .into_iter()
-            .map(|user_role| {
-                if db_user_roles
-                    .iter()
-                    .any(|user_role_inner| user_role_inner.user_id == user_role.user_id)
-                {
-                    Err(errors::StorageError::DuplicateValue {
-                        entity: "user_id",
-                        key: None,
-                    })?
-                }
-                let user_role = storage::UserRole {
-                    id: i32::try_from(db_user_roles.len())
-                        .change_context(errors::StorageError::MockDbError)?,
-                    user_id: user_role.user_id,
-                    merchant_id: user_role.merchant_id,
-                    role_id: user_role.role_id,
-                    status: user_role.status,
-                    created_by: user_role.created_by,
-                    created_at: user_role.created_at,
-                    last_modified: user_role.last_modified,
-                    last_modified_by: user_role.last_modified_by,
-                    org_id: user_role.org_id,
-                    profile_id: None,
-                    entity_id: None,
-                    entity_type: None,
-                    version: enums::UserRoleVersion::V1,
-                };
-                db_user_roles.push(user_role.clone());
-                Ok(user_role)
-            })
-            .collect::<Result<Vec<_>, _>>()
+        if db_user_roles
+            .iter()
+            .any(|user_role_inner| user_role_inner.user_id == user_role.user_id)
+        {
+            Err(errors::StorageError::DuplicateValue {
+                entity: "user_id",
+                key: None,
+            })?
+        }
+        let user_role = storage::UserRole {
+            id: i32::try_from(db_user_roles.len())
+                .change_context(errors::StorageError::MockDbError)?,
+            user_id: user_role.user_id,
+            merchant_id: user_role.merchant_id,
+            role_id: user_role.role_id,
+            status: user_role.status,
+            created_by: user_role.created_by,
+            created_at: user_role.created_at,
+            last_modified: user_role.last_modified,
+            last_modified_by: user_role.last_modified_by,
+            org_id: user_role.org_id,
+            profile_id: None,
+            entity_id: None,
+            entity_type: None,
+            version: enums::UserRoleVersion::V1,
+        };
+        db_user_roles.push(user_role.clone());
+        Ok(user_role)
     }
 
     async fn find_user_role_by_user_id(
diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs
index b271abd1eff..33a1d5f53ca 100644
--- a/crates/router/src/services/authorization/roles/predefined_roles.rs
+++ b/crates/router/src/services/authorization/roles/predefined_roles.rs
@@ -83,9 +83,9 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
             role_name: "organization_admin".to_string(),
             scope: RoleScope::Organization,
             entity_type: EntityType::Organization,
-            is_invitable: false,
-            is_deletable: false,
-            is_updatable: false,
+            is_invitable: true,
+            is_deletable: true,
+            is_updatable: true,
             is_internal: false,
         },
     );
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index 19d682aeef1..a664fc84978 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -30,7 +30,7 @@ use crate::{
         admin,
         errors::{UserErrors, UserResult},
     },
-    db::{user_role::InsertUserRolePayload, GlobalStorageInterface},
+    db::GlobalStorageInterface,
     routes::SessionState,
     services::{self, authentication::UserFromToken, authorization::info},
     types::transformers::ForeignFrom,
@@ -661,26 +661,17 @@ impl NewUser {
         state: SessionState,
         role_id: String,
         user_status: UserStatus,
-        version: Option<UserRoleVersion>,
     ) -> UserResult<UserRole> {
         let org_id = self
             .get_new_merchant()
             .get_new_organization()
             .get_organization_id();
-        let merchant_id = self.get_new_merchant().get_merchant_id();
 
         let org_user_role = self
             .get_no_level_user_role(role_id, user_status)
-            .add_entity(OrganizationLevel {
-                org_id,
-                merchant_id,
-            });
-
-        match version {
-            Some(UserRoleVersion::V1) => org_user_role.insert_in_v1(&state).await,
-            Some(UserRoleVersion::V2) => org_user_role.insert_in_v2(&state).await,
-            None => org_user_role.insert_in_v1_and_v2(&state).await,
-        }
+            .add_entity(OrganizationLevel { org_id });
+
+        org_user_role.insert_in_v2(&state).await
     }
 }
 
@@ -1119,8 +1110,6 @@ pub struct NoLevel;
 #[derive(Clone)]
 pub struct OrganizationLevel {
     pub org_id: id_type::OrganizationId,
-    // Keeping this to allow insertion of org_admins in V1
-    pub merchant_id: id_type::MerchantId,
 }
 
 #[derive(Clone)]
@@ -1174,32 +1163,46 @@ pub struct EntityInfo {
     entity_type: EntityType,
 }
 
-impl<E> NewUserRole<E>
-where
-    E: Clone,
-{
-    fn convert_to_new_v1_role(
-        self,
-        org_id: id_type::OrganizationId,
-        merchant_id: id_type::MerchantId,
-    ) -> UserRoleNew {
-        UserRoleNew {
-            user_id: self.user_id,
-            role_id: self.role_id,
-            status: self.status,
-            created_by: self.created_by,
-            last_modified_by: self.last_modified_by,
-            created_at: self.created_at,
-            last_modified: self.last_modified,
-            org_id: Some(org_id),
-            merchant_id: Some(merchant_id),
+impl From<OrganizationLevel> for EntityInfo {
+    fn from(value: OrganizationLevel) -> Self {
+        Self {
+            entity_id: value.org_id.get_string_repr().to_owned(),
+            entity_type: EntityType::Organization,
+            org_id: value.org_id,
+            merchant_id: None,
+            profile_id: None,
+        }
+    }
+}
+
+impl From<MerchantLevel> for EntityInfo {
+    fn from(value: MerchantLevel) -> Self {
+        Self {
+            entity_id: value.merchant_id.get_string_repr().to_owned(),
+            entity_type: EntityType::Merchant,
+            org_id: value.org_id,
             profile_id: None,
-            entity_id: None,
-            entity_type: None,
-            version: UserRoleVersion::V1,
+            merchant_id: Some(value.merchant_id),
         }
     }
+}
 
+impl From<ProfileLevel> for EntityInfo {
+    fn from(value: ProfileLevel) -> Self {
+        Self {
+            entity_id: value.profile_id.get_string_repr().to_owned(),
+            entity_type: EntityType::Profile,
+            org_id: value.org_id,
+            merchant_id: Some(value.merchant_id),
+            profile_id: Some(value.profile_id),
+        }
+    }
+}
+
+impl<E> NewUserRole<E>
+where
+    E: Clone + Into<EntityInfo>,
+{
     fn convert_to_new_v2_role(self, entity: EntityInfo) -> UserRoleNew {
         UserRoleNew {
             user_id: self.user_id,
@@ -1218,116 +1221,15 @@ where
         }
     }
 
-    async fn insert_v1_and_v2_in_db_and_get_v2(
-        state: &SessionState,
-        v1_role: UserRoleNew,
-        v2_role: UserRoleNew,
-    ) -> UserResult<UserRole> {
-        let inserted_roles = state
-            .store
-            .insert_user_role(InsertUserRolePayload::V1AndV2(Box::new([v1_role, v2_role])))
-            .await
-            .change_context(UserErrors::InternalServerError)?;
-
-        inserted_roles
-            .into_iter()
-            .find(|role| role.version == UserRoleVersion::V2)
-            .ok_or(report!(UserErrors::InternalServerError))
-    }
-}
-
-impl NewUserRole<OrganizationLevel> {
-    pub async fn insert_in_v1(self, state: &SessionState) -> UserResult<UserRole> {
-        let entity = self.entity.clone();
-
-        let new_v1_role = self
-            .clone()
-            .convert_to_new_v1_role(entity.org_id.clone(), entity.merchant_id.clone());
-
-        state
-            .store
-            .insert_user_role(InsertUserRolePayload::OnlyV1(new_v1_role))
-            .await
-            .change_context(UserErrors::InternalServerError)?
-            .pop()
-            .ok_or(report!(UserErrors::InternalServerError))
-    }
-
     pub async fn insert_in_v2(self, state: &SessionState) -> UserResult<UserRole> {
         let entity = self.entity.clone();
 
-        let new_v2_role = self.convert_to_new_v2_role(EntityInfo {
-            org_id: entity.org_id.clone(),
-            merchant_id: None,
-            profile_id: None,
-            entity_id: entity.org_id.get_string_repr().to_owned(),
-            entity_type: EntityType::Organization,
-        });
-        state
-            .store
-            .insert_user_role(InsertUserRolePayload::OnlyV2(new_v2_role))
-            .await
-            .change_context(UserErrors::InternalServerError)?
-            .pop()
-            .ok_or(report!(UserErrors::InternalServerError))
-    }
-
-    pub async fn insert_in_v1_and_v2(self, state: &SessionState) -> UserResult<UserRole> {
-        let entity = self.entity.clone();
-
-        let new_v1_role = self
-            .clone()
-            .convert_to_new_v1_role(entity.org_id.clone(), entity.merchant_id.clone());
+        let new_v2_role = self.convert_to_new_v2_role(entity.into());
 
-        let new_v2_role = self.clone().convert_to_new_v2_role(EntityInfo {
-            org_id: entity.org_id.clone(),
-            merchant_id: None,
-            profile_id: None,
-            entity_id: entity.org_id.get_string_repr().to_owned(),
-            entity_type: EntityType::Organization,
-        });
-
-        Self::insert_v1_and_v2_in_db_and_get_v2(state, new_v1_role, new_v2_role).await
-    }
-}
-
-impl NewUserRole<MerchantLevel> {
-    pub async fn insert_in_v1_and_v2(self, state: &SessionState) -> UserResult<UserRole> {
-        let entity = self.entity.clone();
-
-        let new_v1_role = self
-            .clone()
-            .convert_to_new_v1_role(entity.org_id.clone(), entity.merchant_id.clone());
-
-        let new_v2_role = self.clone().convert_to_new_v2_role(EntityInfo {
-            org_id: entity.org_id.clone(),
-            merchant_id: Some(entity.merchant_id.clone()),
-            profile_id: None,
-            entity_id: entity.merchant_id.get_string_repr().to_owned(),
-            entity_type: EntityType::Merchant,
-        });
-
-        Self::insert_v1_and_v2_in_db_and_get_v2(state, new_v1_role, new_v2_role).await
-    }
-}
-
-impl NewUserRole<ProfileLevel> {
-    pub async fn insert_in_v2(self, state: &SessionState) -> UserResult<UserRole> {
-        let entity = self.entity.clone();
-
-        let new_v2_role = self.convert_to_new_v2_role(EntityInfo {
-            org_id: entity.org_id.clone(),
-            merchant_id: Some(entity.merchant_id.clone()),
-            profile_id: Some(entity.profile_id.clone()),
-            entity_id: entity.profile_id.get_string_repr().to_owned(),
-            entity_type: EntityType::Profile,
-        });
         state
             .store
-            .insert_user_role(InsertUserRolePayload::OnlyV2(new_v2_role))
+            .insert_user_role(new_v2_role)
             .await
-            .change_context(UserErrors::InternalServerError)?
-            .pop()
-            .ok_or(report!(UserErrors::InternalServerError))
+            .change_context(UserErrors::InternalServerError)
     }
 }

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

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

## Description
<!-- Describe your changes in detail -->
Currently we are inserting user_roles in v1 version and v2 version. Since we have completely moved the APIs to support v2, we don't have to insert v1 anymore.

Allow inviting, updating and deleting org_admins.

### 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).
-->
Closes #6184
Closes #6186 

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

All the following APIs will insert only v2 user_roles from now on.
```curl
curl --location 'https://integ-api.hyperswitch.io/user/signup' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "new email",
    "password": "password"
}'
```
```
curl --location 'https://integ-api.hyperswitch.io/user/connect_account' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "new email"
}'
```
```
curl --location 'https://integ-api.hyperswitch.io/user/internal_signup' \
--header 'api-key: test_admin' \
--header 'Content-Type: application/json' \
--data '{
    "name" : "name",
    "email" : "new email",
    "password": "password"
}'
```
```
curl --location 'https://integ-api.hyperswitch.io/user/user/invite_multiple' \
--header 'Content-Type: application/json' \
--data-raw '[
    {
        "email": "email",
        "name": "name",
        "role_id": "merchant_view_only"
    },
    {
        "email": "email 2",
        "name": "name",
        "role_id": "merchant_admin"
    }
]'
```
```
curl --location 'https://integ-api.hyperswitch.io/user/create_merchant' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
    "company_name": "Juspay"
}'
```


You will now be able to invite org_admin
```
curl --location 'https://integ-api.hyperswitch.io/user/user/invite_multiple' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data-raw '[
    {
        "email": "email",
        "name": "name",
        "role_id": "org_admin"
    }
]'
```
## 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
