Skip to main content

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    /// Look up a user's display name by id (first_name + last_name).
110    /// Returns None if the user does not exist.
111    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    /// List all users with their roles.
120    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    /// Create a new user with role assignments.
136    /// Returns `Err("email_exists")` on duplicate email.
137    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        // Build assignments with the real user_id
158        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    /// Update an existing user. Returns `None` if not found.
172    /// Returns `Err("email_exists")` on duplicate email.
173    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    /// Activate a user. Returns `None` if not found.
216    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    /// Deactivate a user. Returns `None` if not found.
226    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    /// Delete a user. Returns `false` if not found.
236    pub async fn delete(&self, id: Uuid) -> Result<bool, String> {
237        self.user_repo.delete(id).await
238    }
239
240    /// Verify a user exists and holds the given role.
241    /// Returns `Err("User not found")` or `Err("User must have role '...' ...")`.
242    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}