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 list_all(&self) -> Result<Vec<UserResponse>, String> {
111 let users = self.user_repo.find_all().await?;
112 let user_ids: Vec<Uuid> = users.iter().map(|u| u.id).collect();
113 let mut roles_map: HashMap<Uuid, Vec<UserRoleAssignment>> =
114 self.role_repo.list_for_users(&user_ids).await?;
115
116 Ok(users
117 .into_iter()
118 .map(|user| {
119 let assignments = roles_map.remove(&user.id).unwrap_or_default();
120 Self::build_response(user, assignments)
121 })
122 .collect())
123 }
124
125 pub async fn create(
128 &self,
129 email: String,
130 password_hash: String,
131 first_name: String,
132 last_name: String,
133 primary_role: UserRole,
134 primary_org: Option<Uuid>,
135 role_assignments: Vec<UserRoleAssignment>,
136 ) -> Result<UserResponse, String> {
137 let user = User::new(
138 email,
139 password_hash,
140 first_name,
141 last_name,
142 primary_role,
143 primary_org,
144 )?;
145 let created = self.user_repo.create(&user).await?;
146
147 let assignments: Vec<UserRoleAssignment> = role_assignments
149 .into_iter()
150 .map(|mut a| {
151 a.user_id = created.id;
152 a
153 })
154 .collect();
155 self.role_repo.replace_all(created.id, &assignments).await?;
156
157 let final_roles = self.role_repo.list_for_user(created.id).await?;
158 Ok(Self::build_response(created, final_roles))
159 }
160
161 pub async fn update(
164 &self,
165 id: Uuid,
166 email: String,
167 first_name: String,
168 last_name: String,
169 primary_role: UserRole,
170 primary_org: Option<Uuid>,
171 password_hash: Option<String>,
172 role_assignments: Vec<UserRoleAssignment>,
173 ) -> Result<Option<UserResponse>, String> {
174 let mut user = match self.user_repo.find_by_id(id).await? {
175 Some(u) => u,
176 None => return Ok(None),
177 };
178
179 user.email = email.trim().to_lowercase();
180 user.first_name = first_name.trim().to_string();
181 user.last_name = last_name.trim().to_string();
182 user.role = primary_role;
183 user.organization_id = primary_org;
184 user.updated_at = Utc::now();
185
186 if let Some(pw) = password_hash {
187 self.user_repo.update_password(id, &pw).await?;
188 }
189
190 self.user_repo.update(&user).await?;
191
192 let assignments: Vec<UserRoleAssignment> = role_assignments
193 .into_iter()
194 .map(|mut a| {
195 a.user_id = id;
196 a
197 })
198 .collect();
199 self.role_repo.replace_all(id, &assignments).await?;
200
201 let final_roles = self.role_repo.list_for_user(id).await?;
202 Ok(Some(Self::build_response(user, final_roles)))
203 }
204
205 pub async fn activate(&self, id: Uuid) -> Result<Option<UserResponse>, String> {
207 let user = match self.user_repo.activate(id).await? {
208 Some(u) => u,
209 None => return Ok(None),
210 };
211 let roles = self.role_repo.list_for_user(id).await?;
212 Ok(Some(Self::build_response(user, roles)))
213 }
214
215 pub async fn deactivate(&self, id: Uuid) -> Result<Option<UserResponse>, String> {
217 let user = match self.user_repo.deactivate(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 delete(&self, id: Uuid) -> Result<bool, String> {
227 self.user_repo.delete(id).await
228 }
229
230 pub async fn validate_user_has_role(&self, user_id: Uuid, role: &str) -> Result<(), String> {
233 self.user_repo
234 .find_by_id(user_id)
235 .await?
236 .ok_or_else(|| "User not found".to_string())?;
237
238 let roles = self.role_repo.list_for_user(user_id).await?;
239 if !roles.iter().any(|r| r.role.to_string() == role) {
240 return Err(format!(
241 "User must have role '{}' to be linked to an owner entity",
242 role
243 ));
244 }
245 Ok(())
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::application::ports::user_repository::MockUserRepo;
253 use crate::application::ports::user_role_repository::MockUserRoleRepo;
254 use chrono::Utc;
255
256 fn make_user(id: Uuid) -> User {
257 User {
258 id,
259 email: "alice@example.com".to_string(),
260 password_hash: "hash".to_string(),
261 first_name: "Alice".to_string(),
262 last_name: "Smith".to_string(),
263 role: UserRole::Syndic,
264 organization_id: Some(Uuid::new_v4()),
265 is_active: true,
266 processing_restricted: false,
267 processing_restricted_at: None,
268 marketing_opt_out: false,
269 marketing_opt_out_at: None,
270 created_at: Utc::now(),
271 updated_at: Utc::now(),
272 }
273 }
274
275 #[tokio::test]
276 async fn test_list_all_returns_users_with_roles() {
277 let user_id = Uuid::new_v4();
278 let user = make_user(user_id);
279
280 let mut mock_user = MockUserRepo::new();
281 mock_user
282 .expect_find_all()
283 .returning(move || Ok(vec![user.clone()]));
284
285 let mut mock_role = MockUserRoleRepo::new();
286 mock_role
287 .expect_list_for_users()
288 .returning(|_| Ok(std::collections::HashMap::new()));
289
290 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
291 let result = uc.list_all().await.unwrap();
292 assert_eq!(result.len(), 1);
293 assert_eq!(result[0].email, "alice@example.com");
294 }
295
296 #[tokio::test]
297 async fn test_activate_not_found() {
298 let mut mock_user = MockUserRepo::new();
299 mock_user.expect_activate().returning(|_| Ok(None));
300
301 let mock_role = MockUserRoleRepo::new();
302 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
303 let result = uc.activate(Uuid::new_v4()).await.unwrap();
304 assert!(result.is_none());
305 }
306
307 #[tokio::test]
308 async fn test_delete_delegates_to_repo() {
309 let mut mock_user = MockUserRepo::new();
310 mock_user.expect_delete().returning(|_| Ok(true));
311
312 let mock_role = MockUserRoleRepo::new();
313 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
314 let result = uc.delete(Uuid::new_v4()).await.unwrap();
315 assert!(result);
316 }
317}