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, 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 pub processing_restricted: bool,
66 pub processing_restricted_at: Option<DateTime<Utc>>,
67
68 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 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 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 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 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 self.validate()
183 .map_err(|e| format!("Validation error: {}", e))?;
184
185 Ok(())
186 }
187
188 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 pub fn unrestrict_processing(&mut self) {
204 self.processing_restricted = false;
205 self.updated_at = Utc::now();
207 }
208
209 pub fn set_marketing_opt_out(&mut self, opt_out: bool) {
212 if opt_out && !self.marketing_opt_out {
213 self.marketing_opt_out = true;
215 self.marketing_opt_out_at = Some(Utc::now());
216 } else if !opt_out && self.marketing_opt_out {
217 self.marketing_opt_out = false;
219 }
221
222 self.updated_at = Utc::now();
223 }
224
225 pub fn can_process_data(&self) -> bool {
227 !self.processing_restricted
228 }
229
230 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 #[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"); assert_eq!(user.first_name, "Jane"); assert_eq!(user.last_name, "Doe"); }
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"); }
400
401 #[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); }
470
471 #[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); }
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 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 assert!(user.can_process_data());
538 assert!(user.can_send_marketing());
539 }
540}