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        // Validate email format BEFORE modifying anything
149        if let Some(ref new_email) = email {
150            let email_normalized = new_email.to_lowercase().trim().to_string();
151            if !email_normalized.contains('@') || email_normalized.len() < 3 {
152                return Err(format!("Invalid email format: {}", new_email));
153            }
154        }
155
156        // Validate names are not empty BEFORE modifying
157        if let Some(ref new_first_name) = first_name {
158            if new_first_name.trim().is_empty() {
159                return Err("First name cannot be empty".to_string());
160            }
161        }
162        if let Some(ref new_last_name) = last_name {
163            if new_last_name.trim().is_empty() {
164                return Err("Last name cannot be empty".to_string());
165            }
166        }
167
168        // Only apply changes after validation passes
169        if let Some(new_email) = email {
170            self.email = new_email.to_lowercase().trim().to_string();
171        }
172        if let Some(new_first_name) = first_name {
173            self.first_name = new_first_name.trim().to_string();
174        }
175        if let Some(new_last_name) = last_name {
176            self.last_name = new_last_name.trim().to_string();
177        }
178
179        self.updated_at = Utc::now();
180
181        // Final validation with full validator
182        self.validate()
183            .map_err(|e| format!("Validation error: {}", e))?;
184
185        Ok(())
186    }
187
188    // GDPR Article 18: Right to Restriction of Processing
189    // Users can request temporary limitation of data processing
190    pub fn restrict_processing(&mut self) -> Result<(), String> {
191        if self.processing_restricted {
192            return Err("Processing is already restricted for this user".to_string());
193        }
194
195        self.processing_restricted = true;
196        self.processing_restricted_at = Some(Utc::now());
197        self.updated_at = Utc::now();
198
199        Ok(())
200    }
201
202    // GDPR Article 18: Unrestrict processing (admin action or legal requirement met)
203    pub fn unrestrict_processing(&mut self) {
204        self.processing_restricted = false;
205        // Keep processing_restricted_at for audit trail
206        self.updated_at = Utc::now();
207    }
208
209    // GDPR Article 21: Right to Object (Marketing opt-out)
210    // Users can object to marketing communications and profiling
211    pub fn set_marketing_opt_out(&mut self, opt_out: bool) {
212        if opt_out && !self.marketing_opt_out {
213            // User is opting out
214            self.marketing_opt_out = true;
215            self.marketing_opt_out_at = Some(Utc::now());
216        } else if !opt_out && self.marketing_opt_out {
217            // User is opting back in
218            self.marketing_opt_out = false;
219            // Keep marketing_opt_out_at for audit trail
220        }
221
222        self.updated_at = Utc::now();
223    }
224
225    // Helper to check if user data processing is allowed
226    pub fn can_process_data(&self) -> bool {
227        !self.processing_restricted
228    }
229
230    // Helper to check if marketing communications are allowed
231    pub fn can_send_marketing(&self) -> bool {
232        !self.marketing_opt_out
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_create_user_success() {
242        let user = User::new(
243            "test@example.com".to_string(),
244            "hashed_password".to_string(),
245            "John".to_string(),
246            "Doe".to_string(),
247            UserRole::Syndic,
248            Some(Uuid::new_v4()),
249        );
250
251        assert!(user.is_ok());
252        let user = user.unwrap();
253        assert_eq!(user.email, "test@example.com");
254        assert_eq!(user.full_name(), "John Doe");
255        assert!(user.is_active);
256    }
257
258    #[test]
259    fn test_create_user_invalid_email() {
260        let user = User::new(
261            "invalid-email".to_string(),
262            "hashed_password".to_string(),
263            "John".to_string(),
264            "Doe".to_string(),
265            UserRole::Syndic,
266            None,
267        );
268
269        assert!(user.is_err());
270    }
271
272    #[test]
273    fn test_update_profile() {
274        let mut user = User::new(
275            "test@example.com".to_string(),
276            "hashed_password".to_string(),
277            "John".to_string(),
278            "Doe".to_string(),
279            UserRole::Syndic,
280            None,
281        )
282        .unwrap();
283
284        let result = user.update_profile("Jane".to_string(), "Smith".to_string());
285        assert!(result.is_ok());
286        assert_eq!(user.full_name(), "Jane Smith");
287    }
288
289    #[test]
290    fn test_deactivate_user() {
291        let mut user = User::new(
292            "test@example.com".to_string(),
293            "hashed_password".to_string(),
294            "John".to_string(),
295            "Doe".to_string(),
296            UserRole::Syndic,
297            None,
298        )
299        .unwrap();
300
301        user.deactivate();
302        assert!(!user.is_active);
303    }
304
305    #[test]
306    fn test_superadmin_can_access_all_buildings() {
307        let user = User::new(
308            "admin@example.com".to_string(),
309            "hashed_password".to_string(),
310            "Admin".to_string(),
311            "User".to_string(),
312            UserRole::SuperAdmin,
313            None,
314        )
315        .unwrap();
316
317        assert!(user.can_access_building(Some(Uuid::new_v4())));
318        assert!(user.can_access_building(None));
319    }
320
321    #[test]
322    fn test_regular_user_access_control() {
323        let org_id = Uuid::new_v4();
324        let user = User::new(
325            "syndic@example.com".to_string(),
326            "hashed_password".to_string(),
327            "John".to_string(),
328            "Syndic".to_string(),
329            UserRole::Syndic,
330            Some(org_id),
331        )
332        .unwrap();
333
334        assert!(user.can_access_building(Some(org_id)));
335        assert!(!user.can_access_building(Some(Uuid::new_v4())));
336    }
337
338    // GDPR Article 16 Tests
339    #[test]
340    fn test_rectify_data_success() {
341        let mut user = User::new(
342            "old@example.com".to_string(),
343            "hashed_password".to_string(),
344            "OldFirst".to_string(),
345            "OldLast".to_string(),
346            UserRole::Owner,
347            None,
348        )
349        .unwrap();
350
351        let result = user.rectify_data(
352            Some("new@example.com".to_string()),
353            Some("NewFirst".to_string()),
354            Some("NewLast".to_string()),
355        );
356
357        assert!(result.is_ok());
358        assert_eq!(user.email, "new@example.com");
359        assert_eq!(user.first_name, "NewFirst");
360        assert_eq!(user.last_name, "NewLast");
361    }
362
363    #[test]
364    fn test_rectify_data_partial() {
365        let mut user = User::new(
366            "test@example.com".to_string(),
367            "hashed_password".to_string(),
368            "John".to_string(),
369            "Doe".to_string(),
370            UserRole::Owner,
371            None,
372        )
373        .unwrap();
374
375        let result = user.rectify_data(None, Some("Jane".to_string()), None);
376
377        assert!(result.is_ok());
378        assert_eq!(user.email, "test@example.com"); // unchanged
379        assert_eq!(user.first_name, "Jane"); // changed
380        assert_eq!(user.last_name, "Doe"); // unchanged
381    }
382
383    #[test]
384    fn test_rectify_data_invalid_email() {
385        let mut user = User::new(
386            "test@example.com".to_string(),
387            "hashed_password".to_string(),
388            "John".to_string(),
389            "Doe".to_string(),
390            UserRole::Owner,
391            None,
392        )
393        .unwrap();
394
395        let result = user.rectify_data(Some("invalid-email".to_string()), None, None);
396
397        assert!(result.is_err());
398        assert_eq!(user.email, "test@example.com"); // unchanged on error
399    }
400
401    // GDPR Article 18 Tests
402    #[test]
403    fn test_restrict_processing_success() {
404        let mut user = User::new(
405            "test@example.com".to_string(),
406            "hashed_password".to_string(),
407            "John".to_string(),
408            "Doe".to_string(),
409            UserRole::Owner,
410            None,
411        )
412        .unwrap();
413
414        assert!(!user.processing_restricted);
415        assert!(user.can_process_data());
416
417        let result = user.restrict_processing();
418
419        assert!(result.is_ok());
420        assert!(user.processing_restricted);
421        assert!(user.processing_restricted_at.is_some());
422        assert!(!user.can_process_data());
423    }
424
425    #[test]
426    fn test_restrict_processing_already_restricted() {
427        let mut user = User::new(
428            "test@example.com".to_string(),
429            "hashed_password".to_string(),
430            "John".to_string(),
431            "Doe".to_string(),
432            UserRole::Owner,
433            None,
434        )
435        .unwrap();
436
437        user.restrict_processing().unwrap();
438
439        let result = user.restrict_processing();
440
441        assert!(result.is_err());
442        assert!(result
443            .unwrap_err()
444            .contains("Processing is already restricted"));
445    }
446
447    #[test]
448    fn test_unrestrict_processing() {
449        let mut user = User::new(
450            "test@example.com".to_string(),
451            "hashed_password".to_string(),
452            "John".to_string(),
453            "Doe".to_string(),
454            UserRole::Owner,
455            None,
456        )
457        .unwrap();
458
459        user.restrict_processing().unwrap();
460        assert!(!user.can_process_data());
461
462        let restriction_timestamp = user.processing_restricted_at;
463
464        user.unrestrict_processing();
465
466        assert!(!user.processing_restricted);
467        assert!(user.can_process_data());
468        assert_eq!(user.processing_restricted_at, restriction_timestamp); // Audit trail preserved
469    }
470
471    // GDPR Article 21 Tests
472    #[test]
473    fn test_set_marketing_opt_out() {
474        let mut user = User::new(
475            "test@example.com".to_string(),
476            "hashed_password".to_string(),
477            "John".to_string(),
478            "Doe".to_string(),
479            UserRole::Owner,
480            None,
481        )
482        .unwrap();
483
484        assert!(!user.marketing_opt_out);
485        assert!(user.can_send_marketing());
486
487        user.set_marketing_opt_out(true);
488
489        assert!(user.marketing_opt_out);
490        assert!(user.marketing_opt_out_at.is_some());
491        assert!(!user.can_send_marketing());
492    }
493
494    #[test]
495    fn test_set_marketing_opt_in_after_opt_out() {
496        let mut user = User::new(
497            "test@example.com".to_string(),
498            "hashed_password".to_string(),
499            "John".to_string(),
500            "Doe".to_string(),
501            UserRole::Owner,
502            None,
503        )
504        .unwrap();
505
506        user.set_marketing_opt_out(true);
507        assert!(!user.can_send_marketing());
508
509        let opt_out_timestamp = user.marketing_opt_out_at;
510
511        user.set_marketing_opt_out(false);
512
513        assert!(!user.marketing_opt_out);
514        assert!(user.can_send_marketing());
515        assert_eq!(user.marketing_opt_out_at, opt_out_timestamp); // Audit trail preserved
516    }
517
518    #[test]
519    fn test_gdpr_defaults_on_new_user() {
520        let user = User::new(
521            "test@example.com".to_string(),
522            "hashed_password".to_string(),
523            "John".to_string(),
524            "Doe".to_string(),
525            UserRole::Owner,
526            None,
527        )
528        .unwrap();
529
530        // GDPR defaults
531        assert!(!user.processing_restricted);
532        assert!(user.processing_restricted_at.is_none());
533        assert!(!user.marketing_opt_out);
534        assert!(user.marketing_opt_out_at.is_none());
535
536        // Helper methods
537        assert!(user.can_process_data());
538        assert!(user.can_send_marketing());
539    }
540}