Bug: feat: update user details api




diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index c0743c8b8fc..40d082d1cad 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -13,7 +13,8 @@ use crate::user::{
     AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
     DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest,
     InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest,
-    SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest,
+    SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest,
+    UserMerchantCreate, VerifyEmailRequest,
 };
 
 impl ApiEventMetric for DashboardEntryResponse {
@@ -54,7 +55,8 @@ common_utils::impl_misc_api_event_type!(
     InviteUserRequest,
     InviteUserResponse,
     VerifyEmailRequest,
-    SendVerifyEmailRequest
+    SendVerifyEmailRequest,
+    UpdateUserAccountDetailsRequest
 );
 
 #[cfg(feature = "dummy_connector")]
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index a04c4fef660..8de6a3c0b4f 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -147,3 +147,9 @@ pub struct VerifyTokenResponse {
     pub merchant_id: String,
     pub user_email: pii::Email,
 }
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct UpdateUserAccountDetailsRequest {
+    pub name: Option<Secret<String>>,
+    pub preferred_merchant_id: Option<String>,
+}
diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs
index d2f9564a530..6b408038ef5 100644
--- a/crates/diesel_models/src/query/user_role.rs
+++ b/crates/diesel_models/src/query/user_role.rs
@@ -19,6 +19,20 @@ impl UserRole {
         .await
     }
 
+    pub async fn find_by_user_id_merchant_id(
+        conn: &PgPooledConn,
+        user_id: String,
+        merchant_id: String,
+    ) -> StorageResult<Self> {
+        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
+            conn,
+            dsl::user_id
+                .eq(user_id)
+                .and(dsl::merchant_id.eq(merchant_id)),
+        )
+        .await
+    }
+
     pub async fn update_by_user_id_merchant_id(
         conn: &PgPooledConn,
         user_id: String,
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index 131d2b18266..c9887e1770f 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -1056,6 +1056,8 @@ diesel::table! {
         is_verified -> Bool,
         created_at -> Timestamp,
         last_modified_at -> Timestamp,
+        #[max_length = 64]
+        preferred_merchant_id -> Nullable<Varchar>,
     }
 }
 
diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs
index c608f2654c6..84fe8710060 100644
--- a/crates/diesel_models/src/user.rs
+++ b/crates/diesel_models/src/user.rs
@@ -19,6 +19,7 @@ pub struct User {
     pub is_verified: bool,
     pub created_at: PrimitiveDateTime,
     pub last_modified_at: PrimitiveDateTime,
+    pub preferred_merchant_id: Option<String>,
 }
 
 #[derive(
@@ -33,6 +34,7 @@ pub struct UserNew {
     pub is_verified: bool,
     pub created_at: Option<PrimitiveDateTime>,
     pub last_modified_at: Option<PrimitiveDateTime>,
+    pub preferred_merchant_id: Option<String>,
 }
 
 #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
@@ -42,6 +44,7 @@ pub struct UserUpdateInternal {
     password: Option<Secret<String>>,
     is_verified: Option<bool>,
     last_modified_at: PrimitiveDateTime,
+    preferred_merchant_id: Option<String>,
 }
 
 #[derive(Debug)]
@@ -51,6 +54,7 @@ pub enum UserUpdate {
         name: Option<String>,
         password: Option<Secret<String>>,
         is_verified: Option<bool>,
+        preferred_merchant_id: Option<String>,
     },
 }
 
@@ -63,16 +67,19 @@ impl From<UserUpdate> for UserUpdateInternal {
                 password: None,
                 is_verified: Some(true),
                 last_modified_at,
+                preferred_merchant_id: None,
             },
             UserUpdate::AccountUpdate {
                 name,
                 password,
                 is_verified,
+                preferred_merchant_id,
             } => Self {
                 name,
                 password,
                 is_verified,
                 last_modified_at,
+                preferred_merchant_id,
             },
         }
     }
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 27a4f67618e..729cef65c20 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -253,6 +253,7 @@ pub async fn change_password(
                 name: None,
                 password: Some(new_password_hash),
                 is_verified: None,
+                preferred_merchant_id: None,
             },
         )
         .await
@@ -330,6 +331,7 @@ pub async fn reset_password(
                 name: None,
                 password: Some(hash_password),
                 is_verified: Some(true),
+                preferred_merchant_id: None,
             },
         )
         .await
@@ -786,3 +788,47 @@ pub async fn verify_token(
         user_email: user.email,
     }))
 }
+
+pub async fn update_user_details(
+    state: AppState,
+    user_token: auth::UserFromToken,
+    req: user_api::UpdateUserAccountDetailsRequest,
+) -> UserResponse<()> {
+    let user: domain::UserFromStorage = state
+        .store
+        .find_user_by_id(&user_token.user_id)
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into();
+
+    let name = req.name.map(domain::UserName::new).transpose()?;
+
+    if let Some(ref preferred_merchant_id) = req.preferred_merchant_id {
+        let _ = state
+            .store
+            .find_user_role_by_user_id_merchant_id(user.get_user_id(), preferred_merchant_id)
+            .await
+            .map_err(|e| {
+                if e.current_context().is_db_not_found() {
+                    e.change_context(UserErrors::MerchantIdNotFound)
+                } else {
+                    e.change_context(UserErrors::InternalServerError)
+                }
+            })?;
+    }
+
+    let user_update = storage_user::UserUpdate::AccountUpdate {
+        name: name.map(|x| x.get_secret().expose()),
+        password: None,
+        is_verified: None,
+        preferred_merchant_id: req.preferred_merchant_id,
+    };
+
+    state
+        .store
+        .update_user_by_user_id(user.get_user_id(), user_update)
+        .await
+        .change_context(UserErrors::InternalServerError)?;
+
+    Ok(ApplicationResponse::StatusOk)
+}
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 19a83088a06..8398c153156 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -1927,12 +1927,24 @@ impl UserRoleInterface for KafkaStore {
     ) -> CustomResult<user_storage::UserRole, errors::StorageError> {
         self.diesel_store.insert_user_role(user_role).await
     }
+
     async fn find_user_role_by_user_id(
         &self,
         user_id: &str,
     ) -> CustomResult<user_storage::UserRole, errors::StorageError> {
         self.diesel_store.find_user_role_by_user_id(user_id).await
     }
+
+    async fn find_user_role_by_user_id_merchant_id(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+    ) -> CustomResult<user_storage::UserRole, errors::StorageError> {
+        self.diesel_store
+            .find_user_role_by_user_id_merchant_id(user_id, merchant_id)
+            .await
+    }
+
     async fn update_user_role_by_user_id_merchant_id(
         &self,
         user_id: &str,
@@ -1943,9 +1955,11 @@ impl UserRoleInterface for KafkaStore {
             .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update)
             .await
     }
+
     async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> {
         self.diesel_store.delete_user_role(user_id).await
     }
+
     async fn list_user_roles_by_user_id(
         &self,
         user_id: &str,
diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs
index e3dda965f9c..ecd71f7e2c9 100644
--- a/crates/router/src/db/user.rs
+++ b/crates/router/src/db/user.rs
@@ -145,6 +145,7 @@ impl UserInterface for MockDb {
             is_verified: user_data.is_verified,
             created_at: user_data.created_at.unwrap_or(time_now),
             last_modified_at: user_data.created_at.unwrap_or(time_now),
+            preferred_merchant_id: user_data.preferred_merchant_id,
         };
         users.push(user.clone());
         Ok(user)
@@ -207,10 +208,14 @@ impl UserInterface for MockDb {
                         name,
                         password,
                         is_verified,
+                        preferred_merchant_id,
                     } => storage::User {
                         name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
                         password: password.clone().unwrap_or(user.password.clone()),
                         is_verified: is_verified.unwrap_or(user.is_verified),
+                        preferred_merchant_id: preferred_merchant_id
+                            .clone()
+                            .or(user.preferred_merchant_id.clone()),
                         ..user.to_owned()
                     },
                 };
diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs
index bf84ae134ea..d8938f9683d 100644
--- a/crates/router/src/db/user_role.rs
+++ b/crates/router/src/db/user_role.rs
@@ -14,16 +14,25 @@ pub trait UserRoleInterface {
         &self,
         user_role: storage::UserRoleNew,
     ) -> CustomResult<storage::UserRole, errors::StorageError>;
+
     async fn find_user_role_by_user_id(
         &self,
         user_id: &str,
     ) -> CustomResult<storage::UserRole, errors::StorageError>;
+
+    async fn find_user_role_by_user_id_merchant_id(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+    ) -> CustomResult<storage::UserRole, errors::StorageError>;
+
     async fn update_user_role_by_user_id_merchant_id(
         &self,
         user_id: &str,
         merchant_id: &str,
         update: storage::UserRoleUpdate,
     ) -> CustomResult<storage::UserRole, errors::StorageError>;
+
     async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError>;
 
     async fn list_user_roles_by_user_id(
@@ -57,6 +66,22 @@ impl UserRoleInterface for Store {
             .into_report()
     }
 
+    async fn find_user_role_by_user_id_merchant_id(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+    ) -> CustomResult<storage::UserRole, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::UserRole::find_by_user_id_merchant_id(
+            &conn,
+            user_id.to_owned(),
+            merchant_id.to_owned(),
+        )
+        .await
+        .map_err(Into::into)
+        .into_report()
+    }
+
     async fn update_user_role_by_user_id_merchant_id(
         &self,
         user_id: &str,
@@ -148,6 +173,24 @@ impl UserRoleInterface for MockDb {
             )
     }
 
+    async fn find_user_role_by_user_id_merchant_id(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+    ) -> CustomResult<storage::UserRole, errors::StorageError> {
+        let user_roles = self.user_roles.lock().await;
+        user_roles
+            .iter()
+            .find(|user_role| user_role.user_id == user_id && user_role.merchant_id == merchant_id)
+            .cloned()
+            .ok_or(
+                errors::StorageError::ValueNotFound(format!(
+                    "No user role available for user_id = {user_id} and merchant_id = {merchant_id}"
+                ))
+                .into(),
+            )
+    }
+
     async fn update_user_role_by_user_id_merchant_id(
         &self,
         user_id: &str,
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 0c489dbe63a..0807fb0800e 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -921,6 +921,7 @@ impl User {
             .service(web::resource("/role/list").route(web::get().to(list_roles)))
             .service(web::resource("/role/{role_id}").route(web::get().to(get_role)))
             .service(web::resource("/user/invite").route(web::post().to(invite_user)))
+            .service(web::resource("/update").route(web::post().to(update_user_account_details)))
             .service(
                 web::resource("/data")
                     .route(web::get().to(get_multiple_dashboard_metadata))
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 12cf76be475..805fb115264 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -178,7 +178,8 @@ impl From<Flow> for ApiIdentifier {
             | Flow::InviteUser
             | Flow::UserSignUpWithMerchantId
             | Flow::VerifyEmail
-            | Flow::VerifyEmailRequest => Self::User,
+            | Flow::VerifyEmailRequest
+            | Flow::UpdateUserAccountDetails => Self::User,
 
             Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
                 Self::UserRole
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 976fd5c9f56..eca32318adf 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -403,3 +403,21 @@ pub async fn verify_recon_token(state: web::Data<AppState>, http_req: HttpReques
     ))
     .await
 }
+
+pub async fn update_user_account_details(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<user_api::UpdateUserAccountDetailsRequest>,
+) -> HttpResponse {
+    let flow = Flow::UpdateUserAccountDetails;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        json_payload.into_inner(),
+        user_core::update_user_details,
+        &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 0d6636e567d..c4e0aa3f3ea 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -331,6 +331,8 @@ pub enum Flow {
     VerifyEmail,
     /// Send verify email
     VerifyEmailRequest,
+    /// Update user account details
+    UpdateUserAccountDetails,
 }
 
 ///
diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql
new file mode 100644
index 00000000000..b9160b6f105
--- /dev/null
+++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+ALTER TABLE users DROP COLUMN preferred_merchant_id;
diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql
new file mode 100644
index 00000000000..77567ce93fa
--- /dev/null
+++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql
@@ -0,0 +1,2 @@
+-- Your SQL goes here
+ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64);

## 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 new column in users table called `preferred_merchant_id`. `preferred_merchant_id` will be used at the time of signin to directly use that `merchant_id` when user has access to multiple merchant accounts.
- This PR adds a new API which enables users to update their name and preferred merchant id.

### Additional Changes

- [x] 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).
-->
To support the highlighted flow in the following diagram.
Note: This PR doesn't have the actual changes of the flow, but has few required changes to make the highlighted flow possible.
```mermaid
flowchart TD
    A["Connect Account"] -- Email --> B{"User Exists"}
    B -- No --> C["Create new \n Org \n Merchant \n User \n User Role"]
    C --> D["Send Verify Email"]
    D --> E["Verify Email Token"]
    B -- Yes --> D
    E --> H{"Is there\n any perferred \nmerchant"}
    F -- ==0 --> I{{"Send list of \nmerchants which\n user has access to\n along with an\n intermediate token"}}
    F -- >1 --> G[Select the first role with active status]
    subgraph Preferred Merchant
	    H -- Yes --> M["Send the token \nwith that merchant_id"]
    end
    H -- No --> F{"How many \n active merchants\n does user have \naccess to"}
    I -- merchant_ids, \nintermediate_token --> O["Accept Invite"]
    O --> P["Change status to active"]
    P --> G
    G --> M
    K["Sign In"] -- Email, Password --> H
```

## 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)?
-->
Postman.
```curl
curl --location 'http://localhost:8080/user/update' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer JWT' \
--data '{
    "name": "user name", // Optional
    "preferred_merchant_id": "merchant_id" // Optional, user should have access to this merchant_id
}'
```

If the api is successful, the db will be updated and the response will be 200 OK.

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