koprogo_api/application/use_cases/
user_use_cases.rs

1use 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    /// List all users with their roles.
110    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    /// Create a new user with role assignments.
126    /// Returns `Err("email_exists")` on duplicate email.
127    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        // Build assignments with the real user_id
148        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    /// Update an existing user. Returns `None` if not found.
162    /// Returns `Err("email_exists")` on duplicate email.
163    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    /// Activate a user. Returns `None` if not found.
206    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    /// Deactivate a user. Returns `None` if not found.
216    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    /// Delete a user. Returns `false` if not found.
226    pub async fn delete(&self, id: Uuid) -> Result<bool, String> {
227        self.user_repo.delete(id).await
228    }
229
230    /// Verify a user exists and holds the given role.
231    /// Returns `Err("User not found")` or `Err("User must have role '...' ...")`.
232    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}