koprogo_api/domain/entities/
user.rs1use 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 Owner,
12}
13
14impl std::fmt::Display for UserRole {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 UserRole::SuperAdmin => write!(f, "superadmin"),
18 UserRole::Syndic => write!(f, "syndic"),
19 UserRole::Accountant => write!(f, "accountant"),
20 UserRole::Owner => write!(f, "owner"),
21 }
22 }
23}
24
25impl std::str::FromStr for UserRole {
26 type Err = String;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 match s.to_lowercase().as_str() {
30 "superadmin" => Ok(UserRole::SuperAdmin),
31 "syndic" => Ok(UserRole::Syndic),
32 "accountant" => Ok(UserRole::Accountant),
33 "owner" => Ok(UserRole::Owner),
34 _ => Err(format!("Invalid user role: {}", s)),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
40pub struct User {
41 pub id: Uuid,
42
43 #[validate(email(message = "Email must be valid"))]
44 pub email: String,
45
46 #[serde(skip_serializing)]
47 pub password_hash: String,
48
49 #[validate(length(min = 2, message = "First name must be at least 2 characters"))]
50 pub first_name: String,
51
52 #[validate(length(min = 2, message = "Last name must be at least 2 characters"))]
53 pub last_name: String,
54
55 pub role: UserRole,
56
57 pub organization_id: Option<Uuid>,
58
59 pub is_active: bool,
60
61 pub created_at: DateTime<Utc>,
62 pub updated_at: DateTime<Utc>,
63}
64
65impl User {
66 pub fn new(
67 email: String,
68 password_hash: String,
69 first_name: String,
70 last_name: String,
71 role: UserRole,
72 organization_id: Option<Uuid>,
73 ) -> Result<Self, String> {
74 let user = Self {
75 id: Uuid::new_v4(),
76 email: email.to_lowercase().trim().to_string(),
77 password_hash,
78 first_name: first_name.trim().to_string(),
79 last_name: last_name.trim().to_string(),
80 role,
81 organization_id,
82 is_active: true,
83 created_at: Utc::now(),
84 updated_at: Utc::now(),
85 };
86
87 user.validate()
88 .map_err(|e| format!("Validation error: {}", e))?;
89
90 Ok(user)
91 }
92
93 pub fn full_name(&self) -> String {
94 format!("{} {}", self.first_name, self.last_name)
95 }
96
97 pub fn update_profile(&mut self, first_name: String, last_name: String) -> Result<(), String> {
98 self.first_name = first_name.trim().to_string();
99 self.last_name = last_name.trim().to_string();
100 self.updated_at = Utc::now();
101
102 self.validate()
103 .map_err(|e| format!("Validation error: {}", e))?;
104
105 Ok(())
106 }
107
108 pub fn deactivate(&mut self) {
109 self.is_active = false;
110 self.updated_at = Utc::now();
111 }
112
113 pub fn activate(&mut self) {
114 self.is_active = true;
115 self.updated_at = Utc::now();
116 }
117
118 pub fn can_access_building(&self, building_org_id: Option<Uuid>) -> bool {
119 match self.role {
120 UserRole::SuperAdmin => true,
121 _ => self.organization_id == building_org_id,
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_create_user_success() {
132 let user = User::new(
133 "test@example.com".to_string(),
134 "hashed_password".to_string(),
135 "John".to_string(),
136 "Doe".to_string(),
137 UserRole::Syndic,
138 Some(Uuid::new_v4()),
139 );
140
141 assert!(user.is_ok());
142 let user = user.unwrap();
143 assert_eq!(user.email, "test@example.com");
144 assert_eq!(user.full_name(), "John Doe");
145 assert!(user.is_active);
146 }
147
148 #[test]
149 fn test_create_user_invalid_email() {
150 let user = User::new(
151 "invalid-email".to_string(),
152 "hashed_password".to_string(),
153 "John".to_string(),
154 "Doe".to_string(),
155 UserRole::Syndic,
156 None,
157 );
158
159 assert!(user.is_err());
160 }
161
162 #[test]
163 fn test_update_profile() {
164 let mut user = User::new(
165 "test@example.com".to_string(),
166 "hashed_password".to_string(),
167 "John".to_string(),
168 "Doe".to_string(),
169 UserRole::Syndic,
170 None,
171 )
172 .unwrap();
173
174 let result = user.update_profile("Jane".to_string(), "Smith".to_string());
175 assert!(result.is_ok());
176 assert_eq!(user.full_name(), "Jane Smith");
177 }
178
179 #[test]
180 fn test_deactivate_user() {
181 let mut user = User::new(
182 "test@example.com".to_string(),
183 "hashed_password".to_string(),
184 "John".to_string(),
185 "Doe".to_string(),
186 UserRole::Syndic,
187 None,
188 )
189 .unwrap();
190
191 user.deactivate();
192 assert!(!user.is_active);
193 }
194
195 #[test]
196 fn test_superadmin_can_access_all_buildings() {
197 let user = User::new(
198 "admin@example.com".to_string(),
199 "hashed_password".to_string(),
200 "Admin".to_string(),
201 "User".to_string(),
202 UserRole::SuperAdmin,
203 None,
204 )
205 .unwrap();
206
207 assert!(user.can_access_building(Some(Uuid::new_v4())));
208 assert!(user.can_access_building(None));
209 }
210
211 #[test]
212 fn test_regular_user_access_control() {
213 let org_id = Uuid::new_v4();
214 let user = User::new(
215 "syndic@example.com".to_string(),
216 "hashed_password".to_string(),
217 "John".to_string(),
218 "Syndic".to_string(),
219 UserRole::Syndic,
220 Some(org_id),
221 )
222 .unwrap();
223
224 assert!(user.can_access_building(Some(org_id)));
225 assert!(!user.can_access_building(Some(Uuid::new_v4())));
226 }
227}