Skip to main content

koprogo_api/domain/plateforme/
user.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4use validator::Validate;
5
6/// Roles métier KoproGo.
7///
8/// Story 3.1 — Sous-rôles métier (FR21 — séparation des pouvoirs comptables).
9/// L'enum distingue :
10/// - `Accountant` (générique, conservé pour compatibilité — encodeur + émetteur)
11/// - `AccountantEncodeur` (saisie comptable amont : Invoice / Quote uniquement)
12/// - `AccountantEmetteur` (sortie financière : Expense / CallForFunds uniquement)
13/// - `CommunityModerator` (modération SEL, sondages, panneau d'affichage)
14/// - Mandataires / spécialistes : Lawyer, Notary, Amo, Architect, Bet, Warden
15///
16/// INV-10 (séparation des pouvoirs) : un encodeur ne peut pas émettre, un émetteur
17/// ne peut pas encoder. Le cumul des deux rôles passe par DEUX assignments
18/// (cf. `UserRoleAssignment`).
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
20pub enum UserRole {
21    SuperAdmin,
22    Syndic,
23    /// Comptable générique (rétrocompat — possède encodeur + émetteur).
24    Accountant,
25    /// Comptable encodeur — saisie comptable amont (facture, devis).
26    AccountantEncodeur,
27    /// Comptable émetteur — sortie financière (charges, appels de fonds).
28    AccountantEmetteur,
29    BoardMember, // Membre du conseil de copropriété
30    Contractor,  // Prestataire externe (plombier, électricien, etc.)
31    Owner,
32    /// Modérateur communauté (SEL, sondages, notices).
33    CommunityModerator,
34    /// Avocat (conseil juridique de la copro).
35    Lawyer,
36    /// Notaire.
37    Notary,
38    /// Assistant Maître d'Ouvrage (AMO).
39    Amo,
40    /// Architecte.
41    Architect,
42    /// Bureau d'études techniques (BET).
43    Bet,
44    /// Concierge / gardien.
45    Warden,
46}
47
48impl std::fmt::Display for UserRole {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            UserRole::SuperAdmin => write!(f, "superadmin"),
52            UserRole::Syndic => write!(f, "syndic"),
53            UserRole::Accountant => write!(f, "accountant"),
54            UserRole::AccountantEncodeur => write!(f, "accountant.encodeur"),
55            UserRole::AccountantEmetteur => write!(f, "accountant.emetteur"),
56            UserRole::BoardMember => write!(f, "board_member"),
57            UserRole::Contractor => write!(f, "contractor"),
58            UserRole::Owner => write!(f, "owner"),
59            UserRole::CommunityModerator => write!(f, "community.moderator"),
60            UserRole::Lawyer => write!(f, "lawyer"),
61            UserRole::Notary => write!(f, "notary"),
62            UserRole::Amo => write!(f, "amo"),
63            UserRole::Architect => write!(f, "architect"),
64            UserRole::Bet => write!(f, "bet"),
65            UserRole::Warden => write!(f, "warden"),
66        }
67    }
68}
69
70impl std::str::FromStr for UserRole {
71    type Err = String;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        // Story 3.1 @edge: trim + lowercase, rejet caractères spéciaux non whitelist.
75        let normalized = s.trim().to_lowercase();
76        if normalized.is_empty() {
77            return Err("Invalid user role: empty string".to_string());
78        }
79        // Whitelist stricte : seuls [a-z0-9_.] tolérés (refuse < > / etc. — @negative).
80        if !normalized
81            .chars()
82            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '.')
83        {
84            return Err(format!("Invalid user role: invalid characters in {}", s));
85        }
86        match normalized.as_str() {
87            "superadmin" => Ok(UserRole::SuperAdmin),
88            "syndic" => Ok(UserRole::Syndic),
89            "accountant" => Ok(UserRole::Accountant),
90            "accountant.encodeur" => Ok(UserRole::AccountantEncodeur),
91            "accountant.emetteur" => Ok(UserRole::AccountantEmetteur),
92            "board_member" => Ok(UserRole::BoardMember),
93            "contractor" => Ok(UserRole::Contractor),
94            "owner" => Ok(UserRole::Owner),
95            "community.moderator" => Ok(UserRole::CommunityModerator),
96            "lawyer" => Ok(UserRole::Lawyer),
97            "notary" => Ok(UserRole::Notary),
98            "amo" => Ok(UserRole::Amo),
99            "architect" => Ok(UserRole::Architect),
100            "bet" => Ok(UserRole::Bet),
101            "warden" => Ok(UserRole::Warden),
102            _ => Err(format!("Invalid user role: {}", s)),
103        }
104    }
105}
106
107impl UserRole {
108    // ===============================================================
109    // === Story 3.1 — Permission helpers (FR21 séparation pouvoirs)
110    // ===============================================================
111
112    /// Peut saisir une facture / devis (entrée comptable amont).
113    ///
114    /// INV-10 : seuls encodeurs comptables + syndic + superadmin (les syndics
115    /// gardent la pleine autorité en l'absence de comptable dédié).
116    pub fn can_encode_invoices(&self) -> bool {
117        matches!(
118            self,
119            UserRole::SuperAdmin
120                | UserRole::Syndic
121                | UserRole::Accountant
122                | UserRole::AccountantEncodeur
123        )
124    }
125
126    /// Peut émettre une charge (sortie financière).
127    ///
128    /// INV-10 : seuls émetteurs comptables + syndic + superadmin.
129    /// Un `AccountantEncodeur` SEUL ne peut PAS émettre — il faut un
130    /// `AccountantEmetteur` (ou cumul des deux assignments).
131    pub fn can_emit_expenses(&self) -> bool {
132        matches!(
133            self,
134            UserRole::SuperAdmin
135                | UserRole::Syndic
136                | UserRole::Accountant
137                | UserRole::AccountantEmetteur
138        )
139    }
140
141    /// Peut créer un appel de fonds.
142    ///
143    /// Même règle que `can_emit_expenses` : c'est une sortie financière.
144    pub fn can_create_call_for_funds(&self) -> bool {
145        matches!(
146            self,
147            UserRole::SuperAdmin
148                | UserRole::Syndic
149                | UserRole::Accountant
150                | UserRole::AccountantEmetteur
151        )
152    }
153
154    /// Peut modérer la communauté (SEL, sondages, panneau d'affichage).
155    pub fn can_moderate_community(&self) -> bool {
156        matches!(
157            self,
158            UserRole::SuperAdmin | UserRole::Syndic | UserRole::CommunityModerator
159        )
160    }
161
162    /// Casquette comptable — générique ou sous-rôle encodeur/émetteur.
163    ///
164    /// Story 5.5 (FR28/FR30, INV-6) : le comptable est un prestataire, pas un
165    /// copropriétaire. `community_access_guard` s'appuie sur ce prédicat pour
166    /// exclure les DEUX sous-rôles des routes communautaires — sauf cumul
167    /// d'un rôle `Owner` distinct (cf. ADR 0052).
168    pub fn is_accountant(&self) -> bool {
169        matches!(
170            self,
171            UserRole::Accountant | UserRole::AccountantEncodeur | UserRole::AccountantEmetteur
172        )
173    }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
177pub struct User {
178    pub id: Uuid,
179
180    #[validate(email(message = "Email must be valid"))]
181    pub email: String,
182
183    #[serde(skip_serializing)]
184    pub password_hash: String,
185
186    #[validate(length(min = 2, message = "First name must be at least 2 characters"))]
187    pub first_name: String,
188
189    #[validate(length(min = 2, message = "Last name must be at least 2 characters"))]
190    pub last_name: String,
191
192    pub role: UserRole,
193
194    pub organization_id: Option<Uuid>,
195
196    pub is_active: bool,
197
198    // GDPR Article 18: Right to Restriction of Processing
199    pub processing_restricted: bool,
200    pub processing_restricted_at: Option<DateTime<Utc>>,
201
202    // GDPR Article 21: Right to Object (Marketing opt-out)
203    pub marketing_opt_out: bool,
204    pub marketing_opt_out_at: Option<DateTime<Utc>>,
205
206    pub created_at: DateTime<Utc>,
207    pub updated_at: DateTime<Utc>,
208}
209
210impl User {
211    pub fn new(
212        email: String,
213        password_hash: String,
214        first_name: String,
215        last_name: String,
216        role: UserRole,
217        organization_id: Option<Uuid>,
218    ) -> Result<Self, String> {
219        let user = Self {
220            id: Uuid::new_v4(),
221            email: email.to_lowercase().trim().to_string(),
222            password_hash,
223            first_name: first_name.trim().to_string(),
224            last_name: last_name.trim().to_string(),
225            role,
226            organization_id,
227            is_active: true,
228            processing_restricted: false,
229            processing_restricted_at: None,
230            marketing_opt_out: false,
231            marketing_opt_out_at: None,
232            created_at: Utc::now(),
233            updated_at: Utc::now(),
234        };
235
236        user.validate()
237            .map_err(|e| format!("Validation error: {}", e))?;
238
239        Ok(user)
240    }
241
242    pub fn full_name(&self) -> String {
243        format!("{} {}", self.first_name, self.last_name)
244    }
245
246    pub fn update_profile(&mut self, first_name: String, last_name: String) -> Result<(), String> {
247        self.first_name = first_name.trim().to_string();
248        self.last_name = last_name.trim().to_string();
249        self.updated_at = Utc::now();
250
251        self.validate()
252            .map_err(|e| format!("Validation error: {}", e))?;
253
254        Ok(())
255    }
256
257    pub fn deactivate(&mut self) {
258        self.is_active = false;
259        self.updated_at = Utc::now();
260    }
261
262    pub fn activate(&mut self) {
263        self.is_active = true;
264        self.updated_at = Utc::now();
265    }
266
267    pub fn can_access_building(&self, building_org_id: Option<Uuid>) -> bool {
268        match self.role {
269            UserRole::SuperAdmin => true,
270            _ => self.organization_id == building_org_id,
271        }
272    }
273
274    // GDPR Article 16: Right to Rectification
275    // Users can correct inaccurate personal data
276    pub fn rectify_data(
277        &mut self,
278        email: Option<String>,
279        first_name: Option<String>,
280        last_name: Option<String>,
281    ) -> Result<(), String> {
282        // At least one field must be provided
283        if email.is_none() && first_name.is_none() && last_name.is_none() {
284            return Err("No fields provided for rectification".to_string());
285        }
286
287        // Validate email format BEFORE modifying anything
288        if let Some(ref new_email) = email {
289            let email_normalized = new_email.to_lowercase().trim().to_string();
290            if !email_normalized.contains('@') || email_normalized.len() < 3 {
291                return Err(format!("Invalid email format: {}", new_email));
292            }
293        }
294
295        // Validate names are not empty BEFORE modifying
296        if let Some(ref new_first_name) = first_name {
297            if new_first_name.trim().is_empty() {
298                return Err("First name cannot be empty".to_string());
299            }
300        }
301        if let Some(ref new_last_name) = last_name {
302            if new_last_name.trim().is_empty() {
303                return Err("Last name cannot be empty".to_string());
304            }
305        }
306
307        // Only apply changes after validation passes
308        if let Some(new_email) = email {
309            self.email = new_email.to_lowercase().trim().to_string();
310        }
311        if let Some(new_first_name) = first_name {
312            self.first_name = new_first_name.trim().to_string();
313        }
314        if let Some(new_last_name) = last_name {
315            self.last_name = new_last_name.trim().to_string();
316        }
317
318        self.updated_at = Utc::now();
319
320        // Final validation with full validator
321        self.validate()
322            .map_err(|e| format!("Validation error: {}", e))?;
323
324        Ok(())
325    }
326
327    // GDPR Article 18: Right to Restriction of Processing
328    // Users can request temporary limitation of data processing
329    pub fn restrict_processing(&mut self) -> Result<(), String> {
330        if self.processing_restricted {
331            return Err("Processing is already restricted for this user".to_string());
332        }
333
334        self.processing_restricted = true;
335        self.processing_restricted_at = Some(Utc::now());
336        self.updated_at = Utc::now();
337
338        Ok(())
339    }
340
341    // GDPR Article 18: Unrestrict processing (admin action or legal requirement met)
342    pub fn unrestrict_processing(&mut self) {
343        self.processing_restricted = false;
344        // Keep processing_restricted_at for audit trail
345        self.updated_at = Utc::now();
346    }
347
348    // GDPR Article 21: Right to Object (Marketing opt-out)
349    // Users can object to marketing communications and profiling
350    pub fn set_marketing_opt_out(&mut self, opt_out: bool) {
351        if opt_out && !self.marketing_opt_out {
352            // User is opting out
353            self.marketing_opt_out = true;
354            self.marketing_opt_out_at = Some(Utc::now());
355        } else if !opt_out && self.marketing_opt_out {
356            // User is opting back in
357            self.marketing_opt_out = false;
358            // Keep marketing_opt_out_at for audit trail
359        }
360
361        self.updated_at = Utc::now();
362    }
363
364    // Helper to check if user data processing is allowed
365    pub fn can_process_data(&self) -> bool {
366        !self.processing_restricted
367    }
368
369    // Helper to check if marketing communications are allowed
370    pub fn can_send_marketing(&self) -> bool {
371        !self.marketing_opt_out
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use std::str::FromStr;
379
380    // ============================================================
381    // === Story 3.1 — UserRole sous-rôles + helpers — 4 catégories
382    // ============================================================
383
384    // --- @happy : parsing canonique + helpers nominaux ---
385
386    #[test]
387    fn happy_parse_accountant_encodeur() {
388        assert_eq!(
389            UserRole::from_str("accountant.encodeur").unwrap(),
390            UserRole::AccountantEncodeur
391        );
392    }
393
394    #[test]
395    fn happy_parse_accountant_emetteur() {
396        assert_eq!(
397            UserRole::from_str("accountant.emetteur").unwrap(),
398            UserRole::AccountantEmetteur
399        );
400    }
401
402    #[test]
403    fn happy_parse_community_moderator() {
404        assert_eq!(
405            UserRole::from_str("community.moderator").unwrap(),
406            UserRole::CommunityModerator
407        );
408    }
409
410    #[test]
411    fn happy_parse_mandataires() {
412        assert_eq!(UserRole::from_str("lawyer").unwrap(), UserRole::Lawyer);
413        assert_eq!(UserRole::from_str("notary").unwrap(), UserRole::Notary);
414        assert_eq!(UserRole::from_str("amo").unwrap(), UserRole::Amo);
415        assert_eq!(
416            UserRole::from_str("architect").unwrap(),
417            UserRole::Architect
418        );
419        assert_eq!(UserRole::from_str("bet").unwrap(), UserRole::Bet);
420        assert_eq!(UserRole::from_str("warden").unwrap(), UserRole::Warden);
421    }
422
423    #[test]
424    fn happy_encodeur_can_encode_invoices() {
425        assert!(UserRole::AccountantEncodeur.can_encode_invoices());
426    }
427
428    #[test]
429    fn happy_emetteur_can_emit_expenses() {
430        assert!(UserRole::AccountantEmetteur.can_emit_expenses());
431        assert!(UserRole::AccountantEmetteur.can_create_call_for_funds());
432    }
433
434    #[test]
435    fn happy_community_moderator_can_moderate() {
436        assert!(UserRole::CommunityModerator.can_moderate_community());
437    }
438
439    #[test]
440    fn happy_accountant_subroles_are_accountant() {
441        // Story 5.5 (FR28/FR30, INV-6) : les 3 variantes comptables sont
442        // reconnues — c'est ce prédicat que community_access_guard exploite.
443        assert!(UserRole::Accountant.is_accountant());
444        assert!(UserRole::AccountantEncodeur.is_accountant());
445        assert!(UserRole::AccountantEmetteur.is_accountant());
446    }
447
448    #[test]
449    fn happy_display_round_trip() {
450        for role in [
451            UserRole::SuperAdmin,
452            UserRole::Syndic,
453            UserRole::Accountant,
454            UserRole::AccountantEncodeur,
455            UserRole::AccountantEmetteur,
456            UserRole::BoardMember,
457            UserRole::Contractor,
458            UserRole::Owner,
459            UserRole::CommunityModerator,
460            UserRole::Lawyer,
461            UserRole::Notary,
462            UserRole::Amo,
463            UserRole::Architect,
464            UserRole::Bet,
465            UserRole::Warden,
466        ] {
467            let s = role.to_string();
468            let parsed = UserRole::from_str(&s)
469                .unwrap_or_else(|e| panic!("Round-trip failed for {:?} -> {} : {}", role, s, e));
470            assert_eq!(parsed, role);
471        }
472    }
473
474    // --- @edge : trim/lowercase, syndic legacy still has all powers ---
475
476    #[test]
477    fn edge_parse_accountant_encodeur_trim_uppercase() {
478        assert_eq!(
479            UserRole::from_str("  ACCOUNTANT.ENCODEUR  ").unwrap(),
480            UserRole::AccountantEncodeur
481        );
482    }
483
484    #[test]
485    fn edge_parse_mixed_case_community_moderator() {
486        assert_eq!(
487            UserRole::from_str("CoMmUnItY.MoDeRaToR").unwrap(),
488            UserRole::CommunityModerator
489        );
490    }
491
492    #[test]
493    fn edge_accountant_role_from_str_roundtrip_is_still_accountant() {
494        // Story 5.5 : le prédicat tient après un aller-retour parsing —
495        // le middleware le reçoit toujours via UserRole déjà parsé, jamais
496        // via la chaîne brute, mais la garantie doit être robuste au trim/case.
497        assert!(UserRole::from_str("accountant.encodeur")
498            .unwrap()
499            .is_accountant());
500        assert!(UserRole::from_str("  ACCOUNTANT.EMETTEUR  ")
501            .unwrap()
502            .is_accountant());
503    }
504
505    #[test]
506    fn edge_syndic_keeps_all_finance_powers() {
507        // Syndic = compétence pleine en l'absence de comptable dédié.
508        assert!(UserRole::Syndic.can_encode_invoices());
509        assert!(UserRole::Syndic.can_emit_expenses());
510        assert!(UserRole::Syndic.can_create_call_for_funds());
511        assert!(UserRole::Syndic.can_moderate_community());
512    }
513
514    #[test]
515    fn edge_generic_accountant_keeps_both_powers() {
516        // Rétrocompat : Accountant générique = encodeur + émetteur.
517        assert!(UserRole::Accountant.can_encode_invoices());
518        assert!(UserRole::Accountant.can_emit_expenses());
519        assert!(UserRole::Accountant.can_create_call_for_funds());
520    }
521
522    #[test]
523    fn edge_cumul_encodeur_et_emetteur_via_assignments() {
524        // INV-10 : un user qui cumule les 2 assignments a les pleins droits comptables.
525        // On simule le cumul en testant que chaque rôle apporte sa capacité.
526        let encodeur = UserRole::AccountantEncodeur;
527        let emetteur = UserRole::AccountantEmetteur;
528        // Union des capacités (le call-site itère sur les assignments).
529        let can_encode = encodeur.can_encode_invoices() || emetteur.can_encode_invoices();
530        let can_emit = encodeur.can_emit_expenses() || emetteur.can_emit_expenses();
531        let can_call = encodeur.can_create_call_for_funds() || emetteur.can_create_call_for_funds();
532        assert!(
533            can_encode && can_emit && can_call,
534            "Encodeur+Emetteur cumul should grant all finance powers"
535        );
536    }
537
538    // --- @security : INV-10 séparation des pouvoirs ---
539
540    #[test]
541    fn security_encodeur_cannot_emit_expenses() {
542        // FR21 / INV-10 : un encodeur seul NE peut PAS émettre une charge.
543        assert!(!UserRole::AccountantEncodeur.can_emit_expenses());
544    }
545
546    #[test]
547    fn security_encodeur_cannot_create_call_for_funds() {
548        assert!(!UserRole::AccountantEncodeur.can_create_call_for_funds());
549    }
550
551    #[test]
552    fn security_emetteur_cannot_encode_invoices() {
553        // Symétrique : un émetteur seul NE peut PAS saisir une facture.
554        assert!(!UserRole::AccountantEmetteur.can_encode_invoices());
555    }
556
557    #[test]
558    fn security_owner_has_no_finance_power() {
559        assert!(!UserRole::Owner.can_encode_invoices());
560        assert!(!UserRole::Owner.can_emit_expenses());
561        assert!(!UserRole::Owner.can_create_call_for_funds());
562        assert!(!UserRole::Owner.can_moderate_community());
563    }
564
565    #[test]
566    fn security_community_moderator_has_no_finance_power() {
567        assert!(!UserRole::CommunityModerator.can_emit_expenses());
568        assert!(!UserRole::CommunityModerator.can_encode_invoices());
569        assert!(!UserRole::CommunityModerator.can_create_call_for_funds());
570    }
571
572    #[test]
573    fn security_non_accountant_roles_are_not_flagged_as_accountant() {
574        // INV-6 : le prédicat ne doit pas sur-détecter — un syndic ou un
575        // owner n'est pas un comptable, même s'il porte des pouvoirs
576        // financiers étendus (cf. can_emit_expenses pour Syndic ci-dessus).
577        for role in [
578            UserRole::SuperAdmin,
579            UserRole::Syndic,
580            UserRole::BoardMember,
581            UserRole::Contractor,
582            UserRole::Owner,
583            UserRole::CommunityModerator,
584            UserRole::Lawyer,
585            UserRole::Notary,
586            UserRole::Amo,
587            UserRole::Architect,
588            UserRole::Bet,
589            UserRole::Warden,
590        ] {
591            assert!(
592                !role.is_accountant(),
593                "{} should not be flagged as accountant",
594                role
595            );
596        }
597    }
598
599    #[test]
600    fn security_mandataires_have_no_finance_power() {
601        for role in [
602            UserRole::Lawyer,
603            UserRole::Notary,
604            UserRole::Amo,
605            UserRole::Architect,
606            UserRole::Bet,
607            UserRole::Warden,
608            UserRole::Contractor,
609            UserRole::BoardMember,
610        ] {
611            assert!(
612                !role.can_emit_expenses(),
613                "{} should not be able to emit expenses",
614                role
615            );
616            assert!(
617                !role.can_encode_invoices(),
618                "{} should not be able to encode invoices",
619                role
620            );
621            assert!(
622                !role.can_create_call_for_funds(),
623                "{} should not be able to create call for funds",
624                role
625            );
626            assert!(
627                !role.can_moderate_community(),
628                "{} should not be able to moderate community",
629                role
630            );
631        }
632    }
633
634    // --- @negative : rôles inconnus, vide, caractères spéciaux ---
635
636    #[test]
637    fn negative_unknown_role_rejected() {
638        let err = UserRole::from_str("hackerman").unwrap_err();
639        assert!(
640            err.contains("Invalid user role"),
641            "Unknown role should fail typed: got {}",
642            err
643        );
644    }
645
646    #[test]
647    fn negative_empty_role_rejected() {
648        let err = UserRole::from_str("").unwrap_err();
649        assert!(
650            err.contains("empty") || err.contains("Invalid"),
651            "Empty role should fail: got {}",
652            err
653        );
654    }
655
656    #[test]
657    fn negative_whitespace_only_role_rejected() {
658        assert!(UserRole::from_str("   ").is_err());
659    }
660
661    #[test]
662    fn negative_role_with_special_chars_rejected() {
663        // Tentative d'injection : refusée AVANT le match sur la whitelist.
664        assert!(UserRole::from_str("accountant.<script>").is_err());
665        assert!(UserRole::from_str("accountant';drop").is_err());
666        assert!(UserRole::from_str("accountant/encodeur").is_err());
667    }
668
669    #[test]
670    fn negative_partial_subrole_rejected() {
671        // "accountant.foo" n'est pas dans la whitelist.
672        assert!(UserRole::from_str("accountant.foo").is_err());
673        assert!(UserRole::from_str("community.spam").is_err());
674    }
675
676    // ============================================================
677    // === Tests existants (rétrocompat User entity)
678    // ============================================================
679
680    #[test]
681    fn test_create_user_success() {
682        let user = User::new(
683            "test@example.com".to_string(),
684            "hashed_password".to_string(),
685            "John".to_string(),
686            "Doe".to_string(),
687            UserRole::Syndic,
688            Some(Uuid::new_v4()),
689        );
690
691        assert!(user.is_ok());
692        let user = user.unwrap();
693        assert_eq!(user.email, "test@example.com");
694        assert_eq!(user.full_name(), "John Doe");
695        assert!(user.is_active);
696    }
697
698    #[test]
699    fn test_create_user_invalid_email() {
700        let user = User::new(
701            "invalid-email".to_string(),
702            "hashed_password".to_string(),
703            "John".to_string(),
704            "Doe".to_string(),
705            UserRole::Syndic,
706            None,
707        );
708
709        assert!(user.is_err());
710    }
711
712    #[test]
713    fn test_update_profile() {
714        let mut user = User::new(
715            "test@example.com".to_string(),
716            "hashed_password".to_string(),
717            "John".to_string(),
718            "Doe".to_string(),
719            UserRole::Syndic,
720            None,
721        )
722        .unwrap();
723
724        let result = user.update_profile("Jane".to_string(), "Smith".to_string());
725        assert!(result.is_ok());
726        assert_eq!(user.full_name(), "Jane Smith");
727    }
728
729    #[test]
730    fn test_deactivate_user() {
731        let mut user = User::new(
732            "test@example.com".to_string(),
733            "hashed_password".to_string(),
734            "John".to_string(),
735            "Doe".to_string(),
736            UserRole::Syndic,
737            None,
738        )
739        .unwrap();
740
741        user.deactivate();
742        assert!(!user.is_active);
743    }
744
745    #[test]
746    fn test_superadmin_can_access_all_buildings() {
747        let user = User::new(
748            "admin@example.com".to_string(),
749            "hashed_password".to_string(),
750            "Admin".to_string(),
751            "User".to_string(),
752            UserRole::SuperAdmin,
753            None,
754        )
755        .unwrap();
756
757        assert!(user.can_access_building(Some(Uuid::new_v4())));
758        assert!(user.can_access_building(None));
759    }
760
761    #[test]
762    fn test_regular_user_access_control() {
763        let org_id = Uuid::new_v4();
764        let user = User::new(
765            "syndic@example.com".to_string(),
766            "hashed_password".to_string(),
767            "John".to_string(),
768            "Syndic".to_string(),
769            UserRole::Syndic,
770            Some(org_id),
771        )
772        .unwrap();
773
774        assert!(user.can_access_building(Some(org_id)));
775        assert!(!user.can_access_building(Some(Uuid::new_v4())));
776    }
777
778    // GDPR Article 16 Tests
779    #[test]
780    fn test_rectify_data_success() {
781        let mut user = User::new(
782            "old@example.com".to_string(),
783            "hashed_password".to_string(),
784            "OldFirst".to_string(),
785            "OldLast".to_string(),
786            UserRole::Owner,
787            None,
788        )
789        .unwrap();
790
791        let result = user.rectify_data(
792            Some("new@example.com".to_string()),
793            Some("NewFirst".to_string()),
794            Some("NewLast".to_string()),
795        );
796
797        assert!(result.is_ok());
798        assert_eq!(user.email, "new@example.com");
799        assert_eq!(user.first_name, "NewFirst");
800        assert_eq!(user.last_name, "NewLast");
801    }
802
803    #[test]
804    fn test_rectify_data_partial() {
805        let mut user = User::new(
806            "test@example.com".to_string(),
807            "hashed_password".to_string(),
808            "John".to_string(),
809            "Doe".to_string(),
810            UserRole::Owner,
811            None,
812        )
813        .unwrap();
814
815        let result = user.rectify_data(None, Some("Jane".to_string()), None);
816
817        assert!(result.is_ok());
818        assert_eq!(user.email, "test@example.com"); // unchanged
819        assert_eq!(user.first_name, "Jane"); // changed
820        assert_eq!(user.last_name, "Doe"); // unchanged
821    }
822
823    #[test]
824    fn test_rectify_data_invalid_email() {
825        let mut user = User::new(
826            "test@example.com".to_string(),
827            "hashed_password".to_string(),
828            "John".to_string(),
829            "Doe".to_string(),
830            UserRole::Owner,
831            None,
832        )
833        .unwrap();
834
835        let result = user.rectify_data(Some("invalid-email".to_string()), None, None);
836
837        assert!(result.is_err());
838        assert_eq!(user.email, "test@example.com"); // unchanged on error
839    }
840
841    // GDPR Article 18 Tests
842    #[test]
843    fn test_restrict_processing_success() {
844        let mut user = User::new(
845            "test@example.com".to_string(),
846            "hashed_password".to_string(),
847            "John".to_string(),
848            "Doe".to_string(),
849            UserRole::Owner,
850            None,
851        )
852        .unwrap();
853
854        assert!(!user.processing_restricted);
855        assert!(user.can_process_data());
856
857        let result = user.restrict_processing();
858
859        assert!(result.is_ok());
860        assert!(user.processing_restricted);
861        assert!(user.processing_restricted_at.is_some());
862        assert!(!user.can_process_data());
863    }
864
865    #[test]
866    fn test_restrict_processing_already_restricted() {
867        let mut user = User::new(
868            "test@example.com".to_string(),
869            "hashed_password".to_string(),
870            "John".to_string(),
871            "Doe".to_string(),
872            UserRole::Owner,
873            None,
874        )
875        .unwrap();
876
877        user.restrict_processing().unwrap();
878
879        let result = user.restrict_processing();
880
881        assert!(result.is_err());
882        assert!(result
883            .unwrap_err()
884            .contains("Processing is already restricted"));
885    }
886
887    #[test]
888    fn test_unrestrict_processing() {
889        let mut user = User::new(
890            "test@example.com".to_string(),
891            "hashed_password".to_string(),
892            "John".to_string(),
893            "Doe".to_string(),
894            UserRole::Owner,
895            None,
896        )
897        .unwrap();
898
899        user.restrict_processing().unwrap();
900        assert!(!user.can_process_data());
901
902        let restriction_timestamp = user.processing_restricted_at;
903
904        user.unrestrict_processing();
905
906        assert!(!user.processing_restricted);
907        assert!(user.can_process_data());
908        assert_eq!(user.processing_restricted_at, restriction_timestamp); // Audit trail preserved
909    }
910
911    // GDPR Article 21 Tests
912    #[test]
913    fn test_set_marketing_opt_out() {
914        let mut user = User::new(
915            "test@example.com".to_string(),
916            "hashed_password".to_string(),
917            "John".to_string(),
918            "Doe".to_string(),
919            UserRole::Owner,
920            None,
921        )
922        .unwrap();
923
924        assert!(!user.marketing_opt_out);
925        assert!(user.can_send_marketing());
926
927        user.set_marketing_opt_out(true);
928
929        assert!(user.marketing_opt_out);
930        assert!(user.marketing_opt_out_at.is_some());
931        assert!(!user.can_send_marketing());
932    }
933
934    #[test]
935    fn test_set_marketing_opt_in_after_opt_out() {
936        let mut user = User::new(
937            "test@example.com".to_string(),
938            "hashed_password".to_string(),
939            "John".to_string(),
940            "Doe".to_string(),
941            UserRole::Owner,
942            None,
943        )
944        .unwrap();
945
946        user.set_marketing_opt_out(true);
947        assert!(!user.can_send_marketing());
948
949        let opt_out_timestamp = user.marketing_opt_out_at;
950
951        user.set_marketing_opt_out(false);
952
953        assert!(!user.marketing_opt_out);
954        assert!(user.can_send_marketing());
955        assert_eq!(user.marketing_opt_out_at, opt_out_timestamp); // Audit trail preserved
956    }
957
958    #[test]
959    fn test_gdpr_defaults_on_new_user() {
960        let user = User::new(
961            "test@example.com".to_string(),
962            "hashed_password".to_string(),
963            "John".to_string(),
964            "Doe".to_string(),
965            UserRole::Owner,
966            None,
967        )
968        .unwrap();
969
970        // GDPR defaults
971        assert!(!user.processing_restricted);
972        assert!(user.processing_restricted_at.is_none());
973        assert!(!user.marketing_opt_out);
974        assert!(user.marketing_opt_out_at.is_none());
975
976        // Helper methods
977        assert!(user.can_process_data());
978        assert!(user.can_send_marketing());
979    }
980}