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 email.is_none() && first_name.is_none() && last_name.is_none() {
150 return Err("No fields provided for rectification".to_string());
151 }
152
153 if let Some(ref new_email) = email {
155 let email_normalized = new_email.to_lowercase().trim().to_string();
156 if !email_normalized.contains('@') || email_normalized.len() < 3 {
157 return Err(format!("Invalid email format: {}", new_email));
158 }
159 }
160
161 if let Some(ref new_first_name) = first_name {
163 if new_first_name.trim().is_empty() {
164 return Err("First name cannot be empty".to_string());
165 }
166 }
167 if let Some(ref new_last_name) = last_name {
168 if new_last_name.trim().is_empty() {
169 return Err("Last name cannot be empty".to_string());
170 }
171 }
172
173 if let Some(new_email) = email {
175 self.email = new_email.to_lowercase().trim().to_string();
176 }
177 if let Some(new_first_name) = first_name {
178 self.first_name = new_first_name.trim().to_string();
179 }
180 if let Some(new_last_name) = last_name {
181 self.last_name = new_last_name.trim().to_string();
182 }
183
184 self.updated_at = Utc::now();
185
186 self.validate()
188 .map_err(|e| format!("Validation error: {}", e))?;
189
190 Ok(())
191 }
192
193 pub fn restrict_processing(&mut self) -> Result<(), String> {
196 if self.processing_restricted {
197 return Err("Processing is already restricted for this user".to_string());
198 }
199
200 self.processing_restricted = true;
201 self.processing_restricted_at = Some(Utc::now());
202 self.updated_at = Utc::now();
203
204 Ok(())
205 }
206
207 pub fn unrestrict_processing(&mut self) {
209 self.processing_restricted = false;
210 self.updated_at = Utc::now();
212 }
213
214 pub fn set_marketing_opt_out(&mut self, opt_out: bool) {
217 if opt_out && !self.marketing_opt_out {
218 self.marketing_opt_out = true;
220 self.marketing_opt_out_at = Some(Utc::now());
221 } else if !opt_out && self.marketing_opt_out {
222 self.marketing_opt_out = false;
224 }
226
227 self.updated_at = Utc::now();
228 }
229
230 pub fn can_process_data(&self) -> bool {
232 !self.processing_restricted
233 }
234
235 pub fn can_send_marketing(&self) -> bool {
237 !self.marketing_opt_out
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_create_user_success() {
247 let user = User::new(
248 "test@example.com".to_string(),
249 "hashed_password".to_string(),
250 "John".to_string(),
251 "Doe".to_string(),
252 UserRole::Syndic,
253 Some(Uuid::new_v4()),
254 );
255
256 assert!(user.is_ok());
257 let user = user.unwrap();
258 assert_eq!(user.email, "test@example.com");
259 assert_eq!(user.full_name(), "John Doe");
260 assert!(user.is_active);
261 }
262
263 #[test]
264 fn test_create_user_invalid_email() {
265 let user = User::new(
266 "invalid-email".to_string(),
267 "hashed_password".to_string(),
268 "John".to_string(),
269 "Doe".to_string(),
270 UserRole::Syndic,
271 None,
272 );
273
274 assert!(user.is_err());
275 }
276
277 #[test]
278 fn test_update_profile() {
279 let mut user = User::new(
280 "test@example.com".to_string(),
281 "hashed_password".to_string(),
282 "John".to_string(),
283 "Doe".to_string(),
284 UserRole::Syndic,
285 None,
286 )
287 .unwrap();
288
289 let result = user.update_profile("Jane".to_string(), "Smith".to_string());
290 assert!(result.is_ok());
291 assert_eq!(user.full_name(), "Jane Smith");
292 }
293
294 #[test]
295 fn test_deactivate_user() {
296 let mut user = User::new(
297 "test@example.com".to_string(),
298 "hashed_password".to_string(),
299 "John".to_string(),
300 "Doe".to_string(),
301 UserRole::Syndic,
302 None,
303 )
304 .unwrap();
305
306 user.deactivate();
307 assert!(!user.is_active);
308 }
309
310 #[test]
311 fn test_superadmin_can_access_all_buildings() {
312 let user = User::new(
313 "admin@example.com".to_string(),
314 "hashed_password".to_string(),
315 "Admin".to_string(),
316 "User".to_string(),
317 UserRole::SuperAdmin,
318 None,
319 )
320 .unwrap();
321
322 assert!(user.can_access_building(Some(Uuid::new_v4())));
323 assert!(user.can_access_building(None));
324 }
325
326 #[test]
327 fn test_regular_user_access_control() {
328 let org_id = Uuid::new_v4();
329 let user = User::new(
330 "syndic@example.com".to_string(),
331 "hashed_password".to_string(),
332 "John".to_string(),
333 "Syndic".to_string(),
334 UserRole::Syndic,
335 Some(org_id),
336 )
337 .unwrap();
338
339 assert!(user.can_access_building(Some(org_id)));
340 assert!(!user.can_access_building(Some(Uuid::new_v4())));
341 }
342
343 #[test]
345 fn test_rectify_data_success() {
346 let mut user = User::new(
347 "old@example.com".to_string(),
348 "hashed_password".to_string(),
349 "OldFirst".to_string(),
350 "OldLast".to_string(),
351 UserRole::Owner,
352 None,
353 )
354 .unwrap();
355
356 let result = user.rectify_data(
357 Some("new@example.com".to_string()),
358 Some("NewFirst".to_string()),
359 Some("NewLast".to_string()),
360 );
361
362 assert!(result.is_ok());
363 assert_eq!(user.email, "new@example.com");
364 assert_eq!(user.first_name, "NewFirst");
365 assert_eq!(user.last_name, "NewLast");
366 }
367
368 #[test]
369 fn test_rectify_data_partial() {
370 let mut user = User::new(
371 "test@example.com".to_string(),
372 "hashed_password".to_string(),
373 "John".to_string(),
374 "Doe".to_string(),
375 UserRole::Owner,
376 None,
377 )
378 .unwrap();
379
380 let result = user.rectify_data(None, Some("Jane".to_string()), None);
381
382 assert!(result.is_ok());
383 assert_eq!(user.email, "test@example.com"); assert_eq!(user.first_name, "Jane"); assert_eq!(user.last_name, "Doe"); }
387
388 #[test]
389 fn test_rectify_data_invalid_email() {
390 let mut user = User::new(
391 "test@example.com".to_string(),
392 "hashed_password".to_string(),
393 "John".to_string(),
394 "Doe".to_string(),
395 UserRole::Owner,
396 None,
397 )
398 .unwrap();
399
400 let result = user.rectify_data(Some("invalid-email".to_string()), None, None);
401
402 assert!(result.is_err());
403 assert_eq!(user.email, "test@example.com"); }
405
406 #[test]
408 fn test_restrict_processing_success() {
409 let mut user = User::new(
410 "test@example.com".to_string(),
411 "hashed_password".to_string(),
412 "John".to_string(),
413 "Doe".to_string(),
414 UserRole::Owner,
415 None,
416 )
417 .unwrap();
418
419 assert!(!user.processing_restricted);
420 assert!(user.can_process_data());
421
422 let result = user.restrict_processing();
423
424 assert!(result.is_ok());
425 assert!(user.processing_restricted);
426 assert!(user.processing_restricted_at.is_some());
427 assert!(!user.can_process_data());
428 }
429
430 #[test]
431 fn test_restrict_processing_already_restricted() {
432 let mut user = User::new(
433 "test@example.com".to_string(),
434 "hashed_password".to_string(),
435 "John".to_string(),
436 "Doe".to_string(),
437 UserRole::Owner,
438 None,
439 )
440 .unwrap();
441
442 user.restrict_processing().unwrap();
443
444 let result = user.restrict_processing();
445
446 assert!(result.is_err());
447 assert!(result
448 .unwrap_err()
449 .contains("Processing is already restricted"));
450 }
451
452 #[test]
453 fn test_unrestrict_processing() {
454 let mut user = User::new(
455 "test@example.com".to_string(),
456 "hashed_password".to_string(),
457 "John".to_string(),
458 "Doe".to_string(),
459 UserRole::Owner,
460 None,
461 )
462 .unwrap();
463
464 user.restrict_processing().unwrap();
465 assert!(!user.can_process_data());
466
467 let restriction_timestamp = user.processing_restricted_at;
468
469 user.unrestrict_processing();
470
471 assert!(!user.processing_restricted);
472 assert!(user.can_process_data());
473 assert_eq!(user.processing_restricted_at, restriction_timestamp); }
475
476 #[test]
478 fn test_set_marketing_opt_out() {
479 let mut user = User::new(
480 "test@example.com".to_string(),
481 "hashed_password".to_string(),
482 "John".to_string(),
483 "Doe".to_string(),
484 UserRole::Owner,
485 None,
486 )
487 .unwrap();
488
489 assert!(!user.marketing_opt_out);
490 assert!(user.can_send_marketing());
491
492 user.set_marketing_opt_out(true);
493
494 assert!(user.marketing_opt_out);
495 assert!(user.marketing_opt_out_at.is_some());
496 assert!(!user.can_send_marketing());
497 }
498
499 #[test]
500 fn test_set_marketing_opt_in_after_opt_out() {
501 let mut user = User::new(
502 "test@example.com".to_string(),
503 "hashed_password".to_string(),
504 "John".to_string(),
505 "Doe".to_string(),
506 UserRole::Owner,
507 None,
508 )
509 .unwrap();
510
511 user.set_marketing_opt_out(true);
512 assert!(!user.can_send_marketing());
513
514 let opt_out_timestamp = user.marketing_opt_out_at;
515
516 user.set_marketing_opt_out(false);
517
518 assert!(!user.marketing_opt_out);
519 assert!(user.can_send_marketing());
520 assert_eq!(user.marketing_opt_out_at, opt_out_timestamp); }
522
523 #[test]
524 fn test_gdpr_defaults_on_new_user() {
525 let user = User::new(
526 "test@example.com".to_string(),
527 "hashed_password".to_string(),
528 "John".to_string(),
529 "Doe".to_string(),
530 UserRole::Owner,
531 None,
532 )
533 .unwrap();
534
535 assert!(!user.processing_restricted);
537 assert!(user.processing_restricted_at.is_none());
538 assert!(!user.marketing_opt_out);
539 assert!(user.marketing_opt_out_at.is_none());
540
541 assert!(user.can_process_data());
543 assert!(user.can_send_marketing());
544 }
545}