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, Contractor, 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 pub processing_restricted: bool,
69 pub processing_restricted_at: Option<DateTime<Utc>>,
70
71 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 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 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 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 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 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 self.validate()
191 .map_err(|e| format!("Validation error: {}", e))?;
192
193 Ok(())
194 }
195
196 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 pub fn unrestrict_processing(&mut self) {
212 self.processing_restricted = false;
213 self.updated_at = Utc::now();
215 }
216
217 pub fn set_marketing_opt_out(&mut self, opt_out: bool) {
220 if opt_out && !self.marketing_opt_out {
221 self.marketing_opt_out = true;
223 self.marketing_opt_out_at = Some(Utc::now());
224 } else if !opt_out && self.marketing_opt_out {
225 self.marketing_opt_out = false;
227 }
229
230 self.updated_at = Utc::now();
231 }
232
233 pub fn can_process_data(&self) -> bool {
235 !self.processing_restricted
236 }
237
238 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 #[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"); assert_eq!(user.first_name, "Jane"); assert_eq!(user.last_name, "Doe"); }
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"); }
408
409 #[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); }
478
479 #[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); }
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 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 assert!(user.can_process_data());
546 assert!(user.can_send_marketing());
547 }
548}