Skip to main content

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