koprogo_api/application/use_cases/
user_use_cases.rs1use crate::application::ports::{UserRepository, UserRoleRepository};
2use crate::domain::entities::{User, UserRole, UserRoleAssignment};
3use chrono::{DateTime, Utc};
4use serde::Serialize;
5use std::collections::HashMap;
6use std::sync::Arc;
7use uuid::Uuid;
8
9#[derive(Serialize, Clone)]
10pub struct RoleResponse {
11 pub id: String,
12 pub role: String,
13 pub organization_id: Option<String>,
14 pub is_primary: bool,
15}
16
17#[derive(Serialize, Clone)]
18pub struct UserResponse {
19 pub id: String,
20 pub email: String,
21 pub first_name: String,
22 pub last_name: String,
23 pub role: String,
24 pub organization_id: Option<String>,
25 pub is_active: bool,
26 pub created_at: DateTime<Utc>,
27 pub roles: Vec<RoleResponse>,
28 pub active_role: Option<RoleResponse>,
29}
30
31pub struct UserUseCases {
32 user_repo: Arc<dyn UserRepository>,
33 role_repo: Arc<dyn UserRoleRepository>,
34}
35
36impl UserUseCases {
37 pub fn new(user_repo: Arc<dyn UserRepository>, role_repo: Arc<dyn UserRoleRepository>) -> Self {
38 Self {
39 user_repo,
40 role_repo,
41 }
42 }
43
44 fn to_role_response(a: &UserRoleAssignment) -> RoleResponse {
45 RoleResponse {
46 id: a.id.to_string(),
47 role: a.role.to_string(),
48 organization_id: a.organization_id.map(|id| id.to_string()),
49 is_primary: a.is_primary,
50 }
51 }
52
53 fn fallback_role(role: &str, organization_id: Option<Uuid>) -> RoleResponse {
54 RoleResponse {
55 id: Uuid::new_v4().to_string(),
56 role: role.to_string(),
57 organization_id: organization_id.map(|id| id.to_string()),
58 is_primary: true,
59 }
60 }
61
62 fn ensure_primary(roles: &mut [RoleResponse]) {
63 if roles.is_empty() {
64 return;
65 }
66 if roles.iter().filter(|r| r.is_primary).count() == 0 {
67 roles[0].is_primary = true;
68 }
69 roles.sort_by_key(|r| std::cmp::Reverse(r.is_primary));
70 }
71
72 fn build_response(user: User, assignments: Vec<UserRoleAssignment>) -> UserResponse {
73 let mut roles: Vec<RoleResponse> = if assignments.is_empty() {
74 vec![Self::fallback_role(
75 &user.role.to_string(),
76 user.organization_id,
77 )]
78 } else {
79 assignments.iter().map(Self::to_role_response).collect()
80 };
81
82 Self::ensure_primary(&mut roles);
83 let active_role = roles
84 .iter()
85 .find(|r| r.is_primary)
86 .cloned()
87 .or_else(|| roles.first().cloned());
88
89 UserResponse {
90 id: user.id.to_string(),
91 email: user.email.clone(),
92 first_name: user.first_name.clone(),
93 last_name: user.last_name.clone(),
94 role: active_role
95 .as_ref()
96 .map(|r| r.role.clone())
97 .unwrap_or_else(|| user.role.to_string()),
98 organization_id: active_role
99 .as_ref()
100 .and_then(|r| r.organization_id.clone())
101 .or_else(|| user.organization_id.map(|id| id.to_string())),
102 is_active: user.is_active,
103 created_at: user.created_at,
104 roles,
105 active_role,
106 }
107 }
108
109 pub async fn find_display_name(&self, id: Uuid) -> Result<Option<String>, String> {
112 Ok(self.user_repo.find_by_id(id).await?.map(|u| {
113 format!("{} {}", u.first_name, u.last_name)
114 .trim()
115 .to_string()
116 }))
117 }
118
119 pub async fn list_all(&self) -> Result<Vec<UserResponse>, String> {
121 let users = self.user_repo.find_all().await?;
122 let user_ids: Vec<Uuid> = users.iter().map(|u| u.id).collect();
123 let mut roles_map: HashMap<Uuid, Vec<UserRoleAssignment>> =
124 self.role_repo.list_for_users(&user_ids).await?;
125
126 Ok(users
127 .into_iter()
128 .map(|user| {
129 let assignments = roles_map.remove(&user.id).unwrap_or_default();
130 Self::build_response(user, assignments)
131 })
132 .collect())
133 }
134
135 pub async fn create(
138 &self,
139 email: String,
140 password_hash: String,
141 first_name: String,
142 last_name: String,
143 primary_role: UserRole,
144 primary_org: Option<Uuid>,
145 role_assignments: Vec<UserRoleAssignment>,
146 ) -> Result<UserResponse, String> {
147 let user = User::new(
148 email,
149 password_hash,
150 first_name,
151 last_name,
152 primary_role,
153 primary_org,
154 )?;
155 let created = self.user_repo.create(&user).await?;
156
157 let assignments: Vec<UserRoleAssignment> = role_assignments
159 .into_iter()
160 .map(|mut a| {
161 a.user_id = created.id;
162 a
163 })
164 .collect();
165 self.role_repo.replace_all(created.id, &assignments).await?;
166
167 let final_roles = self.role_repo.list_for_user(created.id).await?;
168 Ok(Self::build_response(created, final_roles))
169 }
170
171 pub async fn update(
174 &self,
175 id: Uuid,
176 email: String,
177 first_name: String,
178 last_name: String,
179 primary_role: UserRole,
180 primary_org: Option<Uuid>,
181 password_hash: Option<String>,
182 role_assignments: Vec<UserRoleAssignment>,
183 ) -> Result<Option<UserResponse>, String> {
184 let mut user = match self.user_repo.find_by_id(id).await? {
185 Some(u) => u,
186 None => return Ok(None),
187 };
188
189 user.email = email.trim().to_lowercase();
190 user.first_name = first_name.trim().to_string();
191 user.last_name = last_name.trim().to_string();
192 user.role = primary_role;
193 user.organization_id = primary_org;
194 user.updated_at = Utc::now();
195
196 if let Some(pw) = password_hash {
197 self.user_repo.update_password(id, &pw).await?;
198 }
199
200 self.user_repo.update(&user).await?;
201
202 let assignments: Vec<UserRoleAssignment> = role_assignments
203 .into_iter()
204 .map(|mut a| {
205 a.user_id = id;
206 a
207 })
208 .collect();
209 self.role_repo.replace_all(id, &assignments).await?;
210
211 let final_roles = self.role_repo.list_for_user(id).await?;
212 Ok(Some(Self::build_response(user, final_roles)))
213 }
214
215 pub async fn activate(&self, id: Uuid) -> Result<Option<UserResponse>, String> {
217 let user = match self.user_repo.activate(id).await? {
218 Some(u) => u,
219 None => return Ok(None),
220 };
221 let roles = self.role_repo.list_for_user(id).await?;
222 Ok(Some(Self::build_response(user, roles)))
223 }
224
225 pub async fn deactivate(&self, id: Uuid) -> Result<Option<UserResponse>, String> {
227 let user = match self.user_repo.deactivate(id).await? {
228 Some(u) => u,
229 None => return Ok(None),
230 };
231 let roles = self.role_repo.list_for_user(id).await?;
232 Ok(Some(Self::build_response(user, roles)))
233 }
234
235 pub async fn delete(&self, id: Uuid) -> Result<bool, String> {
237 self.user_repo.delete(id).await
238 }
239
240 pub async fn validate_user_has_role(&self, user_id: Uuid, role: &str) -> Result<(), String> {
243 self.user_repo
244 .find_by_id(user_id)
245 .await?
246 .ok_or_else(|| "User not found".to_string())?;
247
248 let roles = self.role_repo.list_for_user(user_id).await?;
249 if !roles.iter().any(|r| r.role.to_string() == role) {
250 return Err(format!(
251 "User must have role '{}' to be linked to an owner entity",
252 role
253 ));
254 }
255 Ok(())
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::application::ports::user_repository::MockUserRepo;
263 use crate::application::ports::user_role_repository::MockUserRoleRepo;
264 use chrono::Utc;
265
266 fn make_user(id: Uuid) -> User {
267 User {
268 id,
269 email: "alice@example.com".to_string(),
270 password_hash: "hash".to_string(),
271 first_name: "Alice".to_string(),
272 last_name: "Smith".to_string(),
273 role: UserRole::Syndic,
274 organization_id: Some(Uuid::new_v4()),
275 is_active: true,
276 processing_restricted: false,
277 processing_restricted_at: None,
278 marketing_opt_out: false,
279 marketing_opt_out_at: None,
280 created_at: Utc::now(),
281 updated_at: Utc::now(),
282 }
283 }
284
285 #[tokio::test]
286 async fn test_list_all_returns_users_with_roles() {
287 let user_id = Uuid::new_v4();
288 let user = make_user(user_id);
289
290 let mut mock_user = MockUserRepo::new();
291 mock_user
292 .expect_find_all()
293 .returning(move || Ok(vec![user.clone()]));
294
295 let mut mock_role = MockUserRoleRepo::new();
296 mock_role
297 .expect_list_for_users()
298 .returning(|_| Ok(std::collections::HashMap::new()));
299
300 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
301 let result = uc.list_all().await.unwrap();
302 assert_eq!(result.len(), 1);
303 assert_eq!(result[0].email, "alice@example.com");
304 }
305
306 #[tokio::test]
307 async fn test_activate_not_found() {
308 let mut mock_user = MockUserRepo::new();
309 mock_user.expect_activate().returning(|_| Ok(None));
310
311 let mock_role = MockUserRoleRepo::new();
312 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
313 let result = uc.activate(Uuid::new_v4()).await.unwrap();
314 assert!(result.is_none());
315 }
316
317 #[tokio::test]
318 async fn test_delete_delegates_to_repo() {
319 let mut mock_user = MockUserRepo::new();
320 mock_user.expect_delete().returning(|_| Ok(true));
321
322 let mock_role = MockUserRoleRepo::new();
323 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
324 let result = uc.delete(Uuid::new_v4()).await.unwrap();
325 assert!(result);
326 }
327}