Bug: Add support to create profile level custom role



Closes issue : https://github.com/juspay/hyperswitch/issues/7740
diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs
index ba63dd61a0c..67fb04099ce 100644
--- a/crates/diesel_models/src/role.rs
+++ b/crates/diesel_models/src/role.rs
@@ -9,7 +9,7 @@ use crate::{enums, schema::roles};
 pub struct Role {
     pub role_name: String,
     pub role_id: String,
-    pub merchant_id: id_type::MerchantId,
+    pub merchant_id: Option<id_type::MerchantId>,
     pub org_id: id_type::OrganizationId,
     #[diesel(deserialize_as = super::DieselArray<enums::PermissionGroup>)]
     pub groups: Vec<enums::PermissionGroup>,
@@ -28,7 +28,7 @@ pub struct Role {
 pub struct RoleNew {
     pub role_name: String,
     pub role_id: String,
-    pub merchant_id: id_type::MerchantId,
+    pub merchant_id: Option<id_type::MerchantId>,
     pub org_id: id_type::OrganizationId,
     #[diesel(deserialize_as = super::DieselArray<enums::PermissionGroup>)]
     pub groups: Vec<enums::PermissionGroup>,
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index 374b341a47e..c25acc386f5 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -1361,7 +1361,7 @@ diesel::table! {
         #[max_length = 64]
         role_id -> Varchar,
         #[max_length = 64]
-        merchant_id -> Varchar,
+        merchant_id -> Nullable<Varchar>,
         #[max_length = 64]
         org_id -> Varchar,
         groups -> Array<Nullable<Text>>,
diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs
index 9a72f663e4f..8cc0dcf7ed6 100644
--- a/crates/diesel_models/src/schema_v2.rs
+++ b/crates/diesel_models/src/schema_v2.rs
@@ -1289,7 +1289,7 @@ diesel::table! {
         #[max_length = 64]
         role_id -> Varchar,
         #[max_length = 64]
-        merchant_id -> Varchar,
+        merchant_id -> Nullable<Varchar>,
         #[max_length = 64]
         org_id -> Varchar,
         groups -> Array<Nullable<Text>>,
diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs
index 34f0e8187ca..9bb8be9cc6e 100644
--- a/crates/router/src/core/user_role/role.rs
+++ b/crates/router/src/core/user_role/role.rs
@@ -78,13 +78,6 @@ pub async fn create_role(
         return Err(report!(UserErrors::InvalidRoleOperation))
             .attach_printable("User trying to create org level custom role");
     }
-
-    // TODO: Remove in PR custom-role-write-pr
-    if matches!(role_entity_type, EntityType::Profile) {
-        return Err(report!(UserErrors::InvalidRoleOperation))
-            .attach_printable("User trying to create profile level custom role");
-    }
-
     let requestor_entity_from_role_scope = EntityType::from(req.role_scope);
 
     if requestor_entity_from_role_scope < role_entity_type {
@@ -120,13 +113,15 @@ pub async fn create_role(
     .await?;
 
     let (org_id, merchant_id, profile_id) = match role_entity_type {
-        EntityType::Organization | EntityType::Tenant => {
-            (user_from_token.org_id, user_from_token.merchant_id, None)
-        }
-        EntityType::Merchant => (user_from_token.org_id, user_from_token.merchant_id, None),
+        EntityType::Organization | EntityType::Tenant => (user_from_token.org_id, None, None),
+        EntityType::Merchant => (
+            user_from_token.org_id,
+            Some(user_from_token.merchant_id),
+            None,
+        ),
         EntityType::Profile => (
             user_from_token.org_id,
-            user_from_token.merchant_id,
+            Some(user_from_token.merchant_id),
             Some(user_from_token.profile_id),
         ),
     };
diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs
index 532a59f5149..ecb962d8d29 100644
--- a/crates/router/src/db/role.rs
+++ b/crates/router/src/db/role.rs
@@ -265,7 +265,13 @@ impl RoleInterface for MockDb {
                     && (role.tenant_id == *tenant_id)
                     && role.org_id == *org_id
                     && ((role.scope == RoleScope::Organization)
-                        || (role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant)
+                        || (role
+                            .merchant_id
+                            .as_ref()
+                            .is_some_and(|merchant_id_from_role| {
+                                merchant_id_from_role == merchant_id
+                                    && role.scope == RoleScope::Merchant
+                            }))
                         || (role
                             .profile_id
                             .as_ref()
@@ -369,10 +375,10 @@ impl RoleInterface for MockDb {
         let roles_list: Vec<_> = roles
             .iter()
             .filter(|role| {
-                let matches_merchant = match merchant_id {
-                    Some(merchant_id) => role.merchant_id == *merchant_id,
-                    None => true,
-                };
+                let matches_merchant = merchant_id
+                    .zip(role.merchant_id.as_ref())
+                    .map(|(merchant_id, role_merchant_id)| merchant_id == role_merchant_id)
+                    .unwrap_or(true);
 
                 matches_merchant
                     && role.org_id == *org_id
@@ -420,17 +426,26 @@ impl RoleInterface for MockDb {
                         vec![EntityType::Merchant]
                     };
 
+                    let matches_merchant = role
+                        .merchant_id
+                        .as_ref()
+                        .is_some_and(|merchant_id_from_role| merchant_id_from_role == merchant_id);
+
                     role.tenant_id == tenant_id
                         && role.org_id == org_id
-                        && (role.scope == RoleScope::Organization
-                            || role.merchant_id == *merchant_id)
+                        && (role.scope == RoleScope::Organization || matches_merchant)
                         && entity_in_vec.contains(&role.entity_type)
                 }
                 storage::ListRolesByEntityPayload::Profile(merchant_id, profile_id) => {
                     let entity_in_vec = [EntityType::Profile];
 
                     let matches_merchant =
-                        role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant;
+                        role.merchant_id
+                            .as_ref()
+                            .is_some_and(|merchant_id_from_role| {
+                                merchant_id_from_role == merchant_id
+                                    && role.scope == RoleScope::Merchant
+                            });
 
                     let matches_profile =
                         role.profile_id
diff --git a/migrations/2025-02-06-122415_udpate-roles/down.sql b/migrations/2025-02-06-122415_udpate-roles/down.sql
new file mode 100644
index 00000000000..be66247abff
--- /dev/null
+++ b/migrations/2025-02-06-122415_udpate-roles/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+ALTER TABLE roles ALTER COLUMN merchant_id SET NOT NULL;
\ No newline at end of file
diff --git a/migrations/2025-02-06-122415_udpate-roles/up.sql b/migrations/2025-02-06-122415_udpate-roles/up.sql
new file mode 100644
index 00000000000..490f5bcb929
--- /dev/null
+++ b/migrations/2025-02-06-122415_udpate-roles/up.sql
@@ -0,0 +1,2 @@
+-- Your SQL goes here
+ALTER TABLE roles ALTER COLUMN merchant_id DROP NOT NULL;
\ No newline at end of file

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

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

## Description
<!-- Describe your changes in detail -->
This pr will enable profile level custom creation from API and also enable org level custom role read


### Additional Changes

- [ ] This PR modifies the API contract
- [X] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
1 . Valid operation 
Request :
```
curl --location 'http://localhost:8080/user/role' \
--header 'authorization: Bearer JWT' \
--header 'cookie: login_token=JWT' \
--data '{
    "role_scope": "profile",
    "entity_type": "profile",
    "groups": [
        "operations_view",
        "operations_manage"
    ],
    "role_name": "profile-role-at-profile-scope"
}'
```

Response 
```
{
    "role_id": "role_vcPMQVtjv0jblNi1tOxB",
    "groups": [
        "operations_view",
        "operations_manage"
    ],
    "role_name": "profile-role-at-profile-scope",
    "role_scope": "profile",
    "entity_type": "profile"
}
```
2. Invalid operation
Request 
```
curl --location 'http://localhost:8080/user/role' \
--header 'authorization: Bearer JWT' \
--header 'cookie: login_token=JWT' \
--data '{
    "role_scope": "profile",
    "entity_type": "merchant",
    "groups": [
        "operations_view",
        "operations_manage"
    ],
    "role_name": "merchant-role-at-profile-scope"
}'
```

Response 
```
{
    "error": {
        "type": "invalid_request",
        "message": "User Role Operation Not Supported",
        "code": "UR_23"
    }
}

```


## 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`
- [ ] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
