koprogo_api/domain/entities/
user.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4use validator::Validate;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum UserRole {
8    SuperAdmin,
9    Syndic,
10    Accountant,
11    BoardMember, // Membre du conseil de copropriété
12    Owner,
13}
14
15impl std::fmt::Display for UserRole {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            UserRole::SuperAdmin => write!(f, "superadmin"),
19            UserRole::Syndic => write!(f, "syndic"),
20            UserRole::Accountant => write!(f, "accountant"),
21            UserRole::BoardMember => write!(f, "board_member"),
22            UserRole::Owner => write!(f, "owner"),
23        }
24    }
25}
26
27impl std::str::FromStr for UserRole {
28    type Err = String;
29
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        match s.to_lowercase().as_str() {
32            "superadmin" => Ok(UserRole::SuperAdmin),
33            "syndic" => Ok(UserRole::Syndic),
34            "accountant" => Ok(UserRole::Accountant),
35            "board_member" => Ok(UserRole::BoardMember),
36            "owner" => Ok(UserRole::Owner),
37            _ => Err(format!("Invalid user role: {}", s)),
38        }
39    }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
43pub struct User {
44    pub id: Uuid,
45
46    #[validate(email(message = "Email must be valid"))]
47    pub email: String,
48
49    #[serde(skip_serializing)]
50    pub password_hash: String,
51
52    #[validate(length(min = 2, message = "First name must be at least 2 characters"))]
53    pub first_name: String,
54
55    #[validate(length(min = 2, message = "Last name must be at least 2 characters"))]
56    pub last_name: String,
57
58    pub role: UserRole,
59
60    pub organization_id: Option<Uuid>,
61
62    pub is_active: bool,
63
64    // GDPR Article 18: Right to Restriction of Processing
65    pub processing_restricted: bool,
66    pub processing_restricted_at: Option<DateTime<Utc>>,
67
68    // GDPR Article 21: Right to Object (Marketing opt-out)
69    pub marketing_opt_out: bool,
70    pub marketing_opt_out_at: Option<DateTime<Utc>>,
71
72    pub created_at: DateTime<Utc>,
73    pub updated_at: DateTime<Utc>,
74}
75
76impl User {
77    pub fn new(
78        email: String,
79        password_hash: String,
80        first_name: String,
81        last_name: String,
82        role: UserRole,
83        organization_id: Option<Uuid>,
84    ) -> Result<Self, String> {
85        let user = Self {
86            id: Uuid::new_v4(),
87            email: email.to_lowercase().trim().to_string(),
88            password_hash,
89            first_name: first_name.trim().to_string(),
90            last_name: last_name.trim().to_string(),
91            role,
92            organization_id,
93            is_active: true,
94            processing_restricted: false,
95            processing_restricted_at: None,
96            marketing_opt_out: false,
97            marketing_opt_out_at: None,
98            created_at: Utc::now(),
99            updated_at: Utc::now(),
100        };
101
102        user.validate()
103            .map_err(|e| format!("Validation error: {}", e))?;
104
105        Ok(user)
106    }
107
108    pub fn full_name(&self) -> String {
109        format!("{} {}", self.first_name, self.last_name)
110    }
111
112    pub fn update_profile(&mut self, first_name: String, last_name: String) -> Result<(), String> {
113        self.first_name = first_name.trim().to_string();
114        self.last_name = last_name.trim().to_string();
115        self.updated_at = Utc::now();
116
117        self.validate()
118            .map_err(|e| format!("Validation error: {}", e))?;
119
120        Ok(())
121    }
122
123    pub fn deactivate(&mut self) {
124        self.is_active = false;
125        self.updated_at = Utc::now();
126    }
127
128    pub fn activate(&mut self) {
129        self.is_active = true;
130        self.updated_at = Utc::now();
131    }
132
133    pub fn can_access_building(&self, building_org_id: Option<Uuid>) -> bool {
134        match self.role {
135            UserRole::SuperAdmin => true,
136            _ => self.organization_id == building_org_id,
137        }
138    }
139
140    // GDPR Article 16: Right to Rectification
141    // Users can correct inaccurate personal data
142    pub fn rectify_data(
143        &mut self,
144        email: Option<String>,
145        first_name: Option<String>,
146        last_name: Option<String>,
147    ) -> Result<(), String> {
148        // At least one field must be provided
149        if email.is_none() && first_name.is_none() && last_name.is_none() {
150            return Err("No fields provided for rectification".to_string());
151        }
152
153        // Validate email format BEFORE modifying anything
154        if let Some(ref new_email) = email {
155            let email_normalized = new_email.to_lowercase().trim().to_string();
156            if !email_normalized.contains('@') || email_normalized.len() < 3 {
157                return Err(format!("Invalid email format: {}", new_email));
158            }
159        }
160
161        // Validate names are not empty BEFORE modifying
162        if let Some(ref new_first_name) = first_name {
163            if new_first_name.trim().is_empty() {
164                return Err("First name cannot be empty".to_string());
165            }
166        }
167        if let Some(ref new_last_name) = last_name {
168            if new_last_name.trim().is_empty() {
169                return Err("Last name cannot be empty".to_string());
170            }
171        }
172
173        // Only apply changes after validation passes
174        if let Some(new_email) = email {
175            self.email = new_email.to_lowercase().trim().to_string();
176        }
177        if let Some(new_first_name) = first_name {
178            self.first_name = new_first_name.trim().to_string();
179        }
180        if let Some(new_last_name) = last_name {
181            self.last_name = new_last_name.trim().to_string();
182        }
183
184        self.updated_at = Utc::now();
185
186        // Final validation with full validator
187        self.validate()
188            .map_err(|e| format!("Validation error: {}", e))?;
189
190        Ok(())
191    }
192
193    // GDPR Article 18: Right to Restriction of Processing
194    // Users can request temporary limitation of data processing
195    pub fn restrict_processing(&mut self) -> Result<(), String> {
196        if self.processing_restricted {
197            return Err("Processing is already restricted for this user".to_string());
198        }
199
200        self.processing_restricted = true;
201        self.processing_restricted_at = Some(Utc::now());
202        self.updated_at = Utc::now();
203
204        Ok(())
205    }
206
207    // GDPR Article 18: Unrestrict processing (admin action or legal requirement met)
208    pub fn unrestrict_processing(&mut self) {
209        self.processing_restricted = false;
210        // Keep processing_restricted_at for audit trail
211        self.updated_at = Utc::now();
212    }
213
214    // GDPR Article 21: Right to Object (Marketing opt-out)
215    // Users can object to marketing communications and profiling
216    pub fn set_marketing_opt_out(&mut self, opt_out: bool) {
217        if opt_out && !self.marketing_opt_out {
218            // User is opting out
219            self.marketing_opt_out = true;
220            self.marketing_opt_out_at = Some(Utc::now());
221        } else if !opt_out && self.marketing_opt_out {
222            // User is opting back in
223            self.marketing_opt_out = false;
224            // Keep marketing_opt_out_at for audit trail
225        }
226
227        self.updated_at = Utc::now();
228    }
229
230    // Helper to check if user data processing is allowed
231    pub fn can_process_data(&self) -> bool {
232        !self.processing_restricted
233    }
234
235    // Helper to check if marketing communications are allowed
236    pub fn can_send_marketing(&self) -> bool {
237        !self.marketing_opt_out
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_create_user_success() {
247        let user = User::new(
248            "test@example.com".to_string(),
249            "hashed_password".to_string(),
250            "John".to_string(),
251            "Doe".to_string(),
252            UserRole::Syndic,
253            Some(Uuid::new_v4()),
254        );
255
256        assert!(user.is_ok());
257        let user = user.unwrap();
258        assert_eq!(user.email, "test@example.com");
259        assert_eq!(user.full_name(), "John Doe");
260        assert!(user.is_active);
261    }
262
263    #[test]
264    fn test_create_user_invalid_email() {
265        let user = User::new(
266            "invalid-email".to_string(),
267            "hashed_password".to_string(),
268            "John".to_string(),
269            "Doe".to_string(),
270            UserRole::Syndic,
271            None,
272        );
273
274        assert!(user.is_err());
275    }
276
277    #[test]
278    fn test_update_profile() {
279        let mut user = User::new(
280            "test@example.com".to_string(),
281            "hashed_password".to_string(),
282            "John".to_string(),
283            "Doe".to_string(),
284            UserRole::Syndic,
285            None,
286        )
287        .unwrap();
288
289        let result = user.update_profile("Jane".to_string(), "Smith".to_string());
290        assert!(result.is_ok());
291        assert_eq!(user.full_name(), "Jane Smith");
292    }
293
294    #[test]
295    fn test_deactivate_user() {
296        let mut user = User::new(
297            "test@example.com".to_string(),
298            "hashed_password".to_string(),
299            "John".to_string(),
300            "Doe".to_string(),
301            UserRole::Syndic,
302            None,
303        )
304        .unwrap();
305
306        user.deactivate();
307        assert!(!user.is_active);
308    }
309
310    #[test]
311    fn test_superadmin_can_access_all_buildings() {
312        let user = User::new(
313            "admin@example.com".to_string(),
314            "hashed_password".to_string(),
315            "Admin".to_string(),
316            "User".to_string(),
317            UserRole::SuperAdmin,
318            None,
319        )
320        .unwrap();
321
322        assert!(user.can_access_building(Some(Uuid::new_v4())));
323        assert!(user.can_access_building(None));
324    }
325
326    #[test]
327    fn test_regular_user_access_control() {
328        let org_id = Uuid::new_v4();
329        let user = User::new(
330            "syndic@example.com".to_string(),
331            "hashed_password".to_string(),
332            "John".to_string(),
333            "Syndic".to_string(),
334            UserRole::Syndic,
335            Some(org_id),
336        )
337        .unwrap();
338
339        assert!(user.can_access_building(Some(org_id)));
340        assert!(!user.can_access_building(Some(Uuid::new_v4())));
341    }
342
343    // GDPR Article 16 Tests
344    #[test]
345    fn test_rectify_data_success() {
346        let mut user = User::new(
347            "old@example.com".to_string(),
348            "hashed_password".to_string(),
349            "OldFirst".to_string(),
350            "OldLast".to_string(),
351            UserRole::Owner,
352            None,
353        )
354        .unwrap();
355
356        let result = user.rectify_data(
357            Some("new@example.com".to_string()),
358            Some("NewFirst".to_string()),
359            Some("NewLast".to_string()),
360        );
361
362        assert!(result.is_ok());
363        assert_eq!(user.email, "new@example.com");
364        assert_eq!(user.first_name, "NewFirst");
365        assert_eq!(user.last_name, "NewLast");
366    }
367
368    #[test]
369    fn test_rectify_data_partial() {
370        let mut user = User::new(
371            "test@example.com".to_string(),
372            "hashed_password".to_string(),
373            "John".to_string(),
374            "Doe".to_string(),
375            UserRole::Owner,
376            None,
377        )
378        .unwrap();
379
380        let result = user.rectify_data(None, Some("Jane".to_string()), None);
381
382        assert!(result.is_ok());
383        assert_eq!(user.email, "test@example.com"); // unchanged
384        assert_eq!(user.first_name, "Jane"); // changed
385        assert_eq!(user.last_name, "Doe"); // unchanged
386    }
387
388    #[test]
389    fn test_rectify_data_invalid_email() {
390        let mut user = User::new(
391            "test@example.com".to_string(),
392            "hashed_password".to_string(),
393            "John".to_string(),
394            "Doe".to_string(),
395            UserRole::Owner,
396            None,
397        )
398        .unwrap();
399
400        let result = user.rectify_data(Some("invalid-email".to_string()), None, None);
401
402        assert!(result.is_err());
403        assert_eq!(user.email, "test@example.com"); // unchanged on error
404    }
405
406    // GDPR Article 18 Tests
407    #[test]
408    fn test_restrict_processing_success() {
409        let mut user = User::new(
410            "test@example.com".to_string(),
411            "hashed_password".to_string(),
412            "John".to_string(),
413            "Doe".to_string(),
414            UserRole::Owner,
415            None,
416        )
417        .unwrap();
418
419        assert!(!user.processing_restricted);
420        assert!(user.can_process_data());
421
422        let result = user.restrict_processing();
423
424        assert!(result.is_ok());
425        assert!(user.processing_restricted);
426        assert!(user.processing_restricted_at.is_some());
427        assert!(!user.can_process_data());
428    }
429
430    #[test]
431    fn test_restrict_processing_already_restricted() {
432        let mut user = User::new(
433            "test@example.com".to_string(),
434            "hashed_password".to_string(),
435            "John".to_string(),
436            "Doe".to_string(),
437            UserRole::Owner,
438            None,
439        )
440        .unwrap();
441
442        user.restrict_processing().unwrap();
443
444        let result = user.restrict_processing();
445
446        assert!(result.is_err());
447        assert!(result
448            .unwrap_err()
449            .contains("Processing is already restricted"));
450    }
451
452    #[test]
453    fn test_unrestrict_processing() {
454        let mut user = User::new(
455            "test@example.com".to_string(),
456            "hashed_password".to_string(),
457            "John".to_string(),
458            "Doe".to_string(),
459            UserRole::Owner,
460            None,
461        )
462        .unwrap();
463
464        user.restrict_processing().unwrap();
465        assert!(!user.can_process_data());
466
467        let restriction_timestamp = user.processing_restricted_at;
468
469        user.unrestrict_processing();
470
471        assert!(!user.processing_restricted);
472        assert!(user.can_process_data());
473        assert_eq!(user.processing_restricted_at, restriction_timestamp); // Audit trail preserved
474    }
475
476    // GDPR Article 21 Tests
477    #[test]
478    fn test_set_marketing_opt_out() {
479        let mut user = User::new(
480            "test@example.com".to_string(),
481            "hashed_password".to_string(),
482            "John".to_string(),
483            "Doe".to_string(),
484            UserRole::Owner,
485            None,
486        )
487        .unwrap();
488
489        assert!(!user.marketing_opt_out);
490        assert!(user.can_send_marketing());
491
492        user.set_marketing_opt_out(true);
493
494        assert!(user.marketing_opt_out);
495        assert!(user.marketing_opt_out_at.is_some());
496        assert!(!user.can_send_marketing());
497    }
498
499    #[test]
500    fn test_set_marketing_opt_in_after_opt_out() {
501        let mut user = User::new(
502            "test@example.com".to_string(),
503            "hashed_password".to_string(),
504            "John".to_string(),
505            "Doe".to_string(),
506            UserRole::Owner,
507            None,
508        )
509        .unwrap();
510
511        user.set_marketing_opt_out(true);
512        assert!(!user.can_send_marketing());
513
514        let opt_out_timestamp = user.marketing_opt_out_at;
515
516        user.set_marketing_opt_out(false);
517
518        assert!(!user.marketing_opt_out);
519        assert!(user.can_send_marketing());
520        assert_eq!(user.marketing_opt_out_at, opt_out_timestamp); // Audit trail preserved
521    }
522
523    #[test]
524    fn test_gdpr_defaults_on_new_user() {
525        let user = User::new(
526            "test@example.com".to_string(),
527            "hashed_password".to_string(),
528            "John".to_string(),
529            "Doe".to_string(),
530            UserRole::Owner,
531            None,
532        )
533        .unwrap();
534
535        // GDPR defaults
536        assert!(!user.processing_restricted);
537        assert!(user.processing_restricted_at.is_none());
538        assert!(!user.marketing_opt_out);
539        assert!(user.marketing_opt_out_at.is_none());
540
541        // Helper methods
542        assert!(user.can_process_data());
543        assert!(user.can_send_marketing());
544    }
545}