1use crate::infrastructure::web::{AppState, AuthenticatedUser};
2use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
3use bcrypt::{hash, DEFAULT_COST};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use sqlx::{Executor, Postgres, Transaction};
8use std::collections::{HashMap, HashSet};
9use uuid::Uuid;
10
11const ALLOWED_ROLES: [&str; 4] = ["superadmin", "syndic", "accountant", "owner"];
12
13#[derive(Serialize, Clone)]
14pub struct RoleResponse {
15 pub id: String,
16 pub role: String,
17 pub organization_id: Option<String>,
18 pub is_primary: bool,
19}
20
21#[derive(Serialize, Clone)]
22pub struct UserResponse {
23 pub id: String,
24 pub email: String,
25 pub first_name: String,
26 pub last_name: String,
27 pub role: String,
28 pub organization_id: Option<String>,
29 pub is_active: bool,
30 pub created_at: DateTime<Utc>,
31 pub roles: Vec<RoleResponse>,
32 pub active_role: Option<RoleResponse>,
33}
34
35#[derive(Deserialize, Clone)]
36pub struct RoleAssignmentRequest {
37 pub role: String,
38 pub organization_id: Option<Uuid>,
39 pub is_primary: Option<bool>,
40}
41
42#[derive(Deserialize)]
43pub struct CreateUserRequest {
44 pub email: String,
45 pub password: String,
46 pub first_name: String,
47 pub last_name: String,
48 pub roles: Option<Vec<RoleAssignmentRequest>>,
49 pub role: Option<String>, pub organization_id: Option<Uuid>, }
52
53#[derive(Deserialize)]
54pub struct UpdateUserRequest {
55 pub email: String,
56 pub first_name: String,
57 pub last_name: String,
58 pub roles: Option<Vec<RoleAssignmentRequest>>,
59 pub role: Option<String>, pub organization_id: Option<Uuid>, pub password: Option<String>,
62}
63
64#[derive(Clone, Debug)]
65struct NormalizedRoleAssignment {
66 id: Uuid,
67 role: String,
68 organization_id: Option<Uuid>,
69 is_primary: bool,
70}
71
72#[get("/users")]
74pub async fn list_users(state: web::Data<AppState>, user: AuthenticatedUser) -> impl Responder {
75 if user.role != "superadmin" {
76 return HttpResponse::Forbidden().json(json!({
77 "error": "Only SuperAdmin can access all users"
78 }));
79 }
80
81 let rows = match sqlx::query!(
82 r#"
83 SELECT id, email, first_name, last_name, role, organization_id, is_active, created_at
84 FROM users
85 ORDER BY created_at DESC
86 "#
87 )
88 .fetch_all(&state.pool)
89 .await
90 {
91 Ok(rows) => rows,
92 Err(e) => {
93 return HttpResponse::InternalServerError().json(json!({
94 "error": format!("Failed to fetch users: {}", e)
95 }))
96 }
97 };
98
99 let user_ids: Vec<Uuid> = rows.iter().map(|row| row.id).collect();
100 let roles_map = match load_roles_for_users(&state.pool, &user_ids).await {
101 Ok(map) => map,
102 Err(e) => {
103 return HttpResponse::InternalServerError().json(json!({
104 "error": format!("Failed to fetch user roles: {}", e)
105 }))
106 }
107 };
108
109 let mut users: Vec<UserResponse> = Vec::with_capacity(user_ids.len());
110 for row in rows {
111 let fallback_role = row.role.clone();
112 let fallback_org = row.organization_id;
113 let mut roles = roles_map
114 .get(&row.id)
115 .cloned()
116 .unwrap_or_else(|| vec![fallback_role_response(fallback_role.clone(), fallback_org)]);
117
118 normalize_primary_role(&mut roles);
119 let active_role = roles
120 .iter()
121 .find(|role| role.is_primary)
122 .cloned()
123 .or_else(|| roles.first().cloned());
124
125 users.push(UserResponse {
126 id: row.id.to_string(),
127 email: row.email,
128 first_name: row.first_name,
129 last_name: row.last_name,
130 role: active_role
131 .as_ref()
132 .map(|r| r.role.clone())
133 .unwrap_or(fallback_role),
134 organization_id: active_role
135 .as_ref()
136 .and_then(|r| r.organization_id.clone())
137 .or_else(|| fallback_org.map(|id| id.to_string())),
138 is_active: row.is_active,
139 created_at: row.created_at,
140 roles,
141 active_role,
142 });
143 }
144
145 HttpResponse::Ok().json(json!({ "data": users }))
146}
147
148#[post("/users")]
150pub async fn create_user(
151 state: web::Data<AppState>,
152 user: AuthenticatedUser,
153 req: web::Json<CreateUserRequest>,
154) -> impl Responder {
155 if user.role != "superadmin" {
156 return HttpResponse::Forbidden().json(json!({
157 "error": "Only SuperAdmin can create users"
158 }));
159 }
160
161 if !req.email.contains('@') {
162 return HttpResponse::BadRequest().json(json!({
163 "error": "Invalid email format"
164 }));
165 }
166
167 if req.first_name.trim().len() < 2 || req.last_name.trim().len() < 2 {
168 return HttpResponse::BadRequest().json(json!({
169 "error": "First and last names must be at least 2 characters"
170 }));
171 }
172
173 if req.password.trim().len() < 6 {
174 return HttpResponse::BadRequest().json(json!({
175 "error": "Password must be at least 6 characters"
176 }));
177 }
178
179 let roles = match normalize_roles(req.roles.clone(), req.role.clone(), req.organization_id) {
180 Ok(roles) => roles,
181 Err(resp) => return resp,
182 };
183
184 let primary_role = roles
185 .iter()
186 .find(|role| role.is_primary)
187 .cloned()
188 .expect("normalized roles always have a primary role");
189
190 let hashed_password = match hash(req.password.trim(), DEFAULT_COST) {
191 Ok(hash) => hash,
192 Err(e) => {
193 return HttpResponse::InternalServerError().json(json!({
194 "error": format!("Failed to hash password: {}", e)
195 }))
196 }
197 };
198
199 let mut tx = match state.pool.begin().await {
200 Ok(tx) => tx,
201 Err(e) => {
202 return HttpResponse::InternalServerError().json(json!({
203 "error": format!("Failed to begin transaction: {}", e)
204 }))
205 }
206 };
207
208 let user_row = match sqlx::query!(
209 r#"
210 INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
211 VALUES ($1, $2, $3, $4, $5, $6, $7, true, NOW(), NOW())
212 RETURNING id
213 "#,
214 Uuid::new_v4(),
215 req.email.trim().to_lowercase(),
216 hashed_password,
217 req.first_name.trim(),
218 req.last_name.trim(),
219 primary_role.role.clone(),
220 primary_role.organization_id
221 )
222 .fetch_one(&mut *tx)
223 .await
224 {
225 Ok(row) => row,
226 Err(sqlx::Error::Database(db_err)) if db_err.is_unique_violation() => {
227 return HttpResponse::BadRequest().json(json!({
228 "error": "Email already exists"
229 }))
230 }
231 Err(e) => {
232 return HttpResponse::InternalServerError().json(json!({
233 "error": format!("Failed to create user: {}", e)
234 }))
235 }
236 };
237
238 if let Err(e) = replace_user_roles(&mut tx, user_row.id, &roles).await {
239 return HttpResponse::InternalServerError().json(json!({
240 "error": format!("Failed to assign roles: {}", e)
241 }));
242 }
243
244 if let Err(e) = tx.commit().await {
245 return HttpResponse::InternalServerError().json(json!({
246 "error": format!("Failed to commit transaction: {}", e)
247 }));
248 }
249
250 match load_user_response(&state.pool, user_row.id).await {
251 Ok(response) => HttpResponse::Created().json(response),
252 Err(e) => HttpResponse::InternalServerError().json(json!({
253 "error": format!("Failed to load created user: {}", e)
254 })),
255 }
256}
257
258#[put("/users/{id}")]
260pub async fn update_user(
261 state: web::Data<AppState>,
262 user: AuthenticatedUser,
263 path: web::Path<Uuid>,
264 req: web::Json<UpdateUserRequest>,
265) -> impl Responder {
266 if user.role != "superadmin" {
267 return HttpResponse::Forbidden().json(json!({
268 "error": "Only SuperAdmin can update users"
269 }));
270 }
271
272 if !req.email.contains('@') {
273 return HttpResponse::BadRequest().json(json!({
274 "error": "Invalid email format"
275 }));
276 }
277
278 if req.first_name.trim().len() < 2 || req.last_name.trim().len() < 2 {
279 return HttpResponse::BadRequest().json(json!({
280 "error": "First and last names must be at least 2 characters"
281 }));
282 }
283
284 if let Some(password) = &req.password {
285 if !password.trim().is_empty() && password.trim().len() < 6 {
286 return HttpResponse::BadRequest().json(json!({
287 "error": "Password must be at least 6 characters"
288 }));
289 }
290 }
291
292 let roles = match normalize_roles(req.roles.clone(), req.role.clone(), req.organization_id) {
293 Ok(roles) => roles,
294 Err(resp) => return resp,
295 };
296
297 let primary_role = roles
298 .iter()
299 .find(|role| role.is_primary)
300 .cloned()
301 .expect("normalized roles always have a primary role");
302
303 let user_id = path.into_inner();
304
305 let mut tx = match state.pool.begin().await {
306 Ok(tx) => tx,
307 Err(e) => {
308 return HttpResponse::InternalServerError().json(json!({
309 "error": format!("Failed to begin transaction: {}", e)
310 }))
311 }
312 };
313
314 if let Some(password) = &req.password {
315 if !password.trim().is_empty() {
316 let hashed = match hash(password.trim(), DEFAULT_COST) {
317 Ok(hash) => hash,
318 Err(e) => {
319 return HttpResponse::InternalServerError().json(json!({
320 "error": format!("Failed to hash password: {}", e)
321 }));
322 }
323 };
324
325 if let Err(e) = tx
326 .execute(sqlx::query!(
327 r#"
328 UPDATE users
329 SET password_hash = $1, updated_at = NOW()
330 WHERE id = $2
331 "#,
332 hashed,
333 user_id
334 ))
335 .await
336 {
337 return HttpResponse::InternalServerError().json(json!({
338 "error": format!("Failed to update password: {}", e)
339 }));
340 }
341 }
342 }
343
344 let updated = match sqlx::query!(
345 r#"
346 UPDATE users
347 SET email = $1,
348 first_name = $2,
349 last_name = $3,
350 role = $4,
351 organization_id = $5,
352 updated_at = NOW()
353 WHERE id = $6
354 RETURNING id
355 "#,
356 req.email.trim().to_lowercase(),
357 req.first_name.trim(),
358 req.last_name.trim(),
359 primary_role.role.clone(),
360 primary_role.organization_id,
361 user_id
362 )
363 .fetch_optional(&mut *tx)
364 .await
365 {
366 Ok(row) => row,
367 Err(sqlx::Error::Database(db_err)) if db_err.is_unique_violation() => {
368 return HttpResponse::BadRequest().json(json!({
369 "error": "Email already exists"
370 }))
371 }
372 Err(e) => {
373 return HttpResponse::InternalServerError().json(json!({
374 "error": format!("Failed to update user: {}", e)
375 }))
376 }
377 };
378
379 if updated.is_none() {
380 return HttpResponse::NotFound().json(json!({
381 "error": "User not found"
382 }));
383 }
384
385 if let Err(e) = replace_user_roles(&mut tx, user_id, &roles).await {
386 return HttpResponse::InternalServerError().json(json!({
387 "error": format!("Failed to update user roles: {}", e)
388 }));
389 }
390
391 if let Err(e) = tx.commit().await {
392 return HttpResponse::InternalServerError().json(json!({
393 "error": format!("Failed to commit transaction: {}", e)
394 }));
395 }
396
397 match load_user_response(&state.pool, user_id).await {
398 Ok(response) => HttpResponse::Ok().json(response),
399 Err(e) => HttpResponse::InternalServerError().json(json!({
400 "error": format!("Failed to load updated user: {}", e)
401 })),
402 }
403}
404
405#[put("/users/{id}/activate")]
407pub async fn activate_user(
408 state: web::Data<AppState>,
409 user: AuthenticatedUser,
410 path: web::Path<Uuid>,
411) -> impl Responder {
412 if user.role != "superadmin" {
413 return HttpResponse::Forbidden().json(json!({
414 "error": "Only SuperAdmin can activate users"
415 }));
416 }
417
418 let user_id = path.into_inner();
419
420 let updated = sqlx::query!(
421 r#"
422 UPDATE users
423 SET is_active = true, updated_at = NOW()
424 WHERE id = $1
425 RETURNING id
426 "#,
427 user_id
428 )
429 .fetch_optional(&state.pool)
430 .await;
431
432 match updated {
433 Ok(Some(_)) => match load_user_response(&state.pool, user_id).await {
434 Ok(response) => HttpResponse::Ok().json(response),
435 Err(e) => HttpResponse::InternalServerError().json(json!({
436 "error": format!("Failed to load user: {}", e)
437 })),
438 },
439 Ok(None) => HttpResponse::NotFound().json(json!({
440 "error": "User not found"
441 })),
442 Err(e) => HttpResponse::InternalServerError().json(json!({
443 "error": format!("Failed to activate user: {}", e)
444 })),
445 }
446}
447
448#[put("/users/{id}/deactivate")]
450pub async fn deactivate_user(
451 state: web::Data<AppState>,
452 user: AuthenticatedUser,
453 path: web::Path<Uuid>,
454) -> impl Responder {
455 if user.role != "superadmin" {
456 return HttpResponse::Forbidden().json(json!({
457 "error": "Only SuperAdmin can deactivate users"
458 }));
459 }
460
461 let user_id = path.into_inner();
462
463 let updated = sqlx::query!(
464 r#"
465 UPDATE users
466 SET is_active = false, updated_at = NOW()
467 WHERE id = $1
468 RETURNING id
469 "#,
470 user_id
471 )
472 .fetch_optional(&state.pool)
473 .await;
474
475 match updated {
476 Ok(Some(_)) => match load_user_response(&state.pool, user_id).await {
477 Ok(response) => HttpResponse::Ok().json(response),
478 Err(e) => HttpResponse::InternalServerError().json(json!({
479 "error": format!("Failed to load user: {}", e)
480 })),
481 },
482 Ok(None) => HttpResponse::NotFound().json(json!({
483 "error": "User not found"
484 })),
485 Err(e) => HttpResponse::InternalServerError().json(json!({
486 "error": format!("Failed to deactivate user: {}", e)
487 })),
488 }
489}
490
491#[delete("/users/{id}")]
493pub async fn delete_user(
494 state: web::Data<AppState>,
495 user: AuthenticatedUser,
496 path: web::Path<Uuid>,
497) -> impl Responder {
498 if user.role != "superadmin" {
499 return HttpResponse::Forbidden().json(json!({
500 "error": "Only SuperAdmin can delete users"
501 }));
502 }
503
504 let user_id = path.into_inner();
505
506 if user.user_id == user_id {
507 return HttpResponse::BadRequest().json(json!({
508 "error": "Cannot delete your own account"
509 }));
510 }
511
512 match sqlx::query!(
513 r#"
514 DELETE FROM users
515 WHERE id = $1
516 "#,
517 user_id
518 )
519 .execute(&state.pool)
520 .await
521 {
522 Ok(result) => {
523 if result.rows_affected() == 0 {
524 HttpResponse::NotFound().json(json!({
525 "error": "User not found"
526 }))
527 } else {
528 HttpResponse::Ok().json(json!({
529 "message": "User deleted successfully"
530 }))
531 }
532 }
533 Err(e) => HttpResponse::InternalServerError().json(json!({
534 "error": format!("Failed to delete user: {}", e)
535 })),
536 }
537}
538
539fn normalize_roles(
540 roles: Option<Vec<RoleAssignmentRequest>>,
541 fallback_role: Option<String>,
542 fallback_org: Option<Uuid>,
543) -> Result<Vec<NormalizedRoleAssignment>, HttpResponse> {
544 let mut entries = roles.unwrap_or_else(|| {
545 fallback_role
546 .map(|role| {
547 vec![RoleAssignmentRequest {
548 role,
549 organization_id: fallback_org,
550 is_primary: Some(true),
551 }]
552 })
553 .unwrap_or_default()
554 });
555
556 if entries.is_empty() {
557 return Err(HttpResponse::BadRequest().json(json!({
558 "error": "At least one role must be specified"
559 })));
560 }
561
562 let mut normalized = Vec::with_capacity(entries.len());
563 let mut seen = HashSet::new();
564 let mut primary_count = 0;
565
566 for entry in entries.drain(..) {
567 let RoleAssignmentRequest {
568 role,
569 organization_id,
570 is_primary,
571 } = entry;
572 let normalized_role = role.trim().to_lowercase();
573 if !ALLOWED_ROLES.contains(&normalized_role.as_str()) {
574 return Err(HttpResponse::BadRequest().json(json!({
575 "error": format!("Invalid role: {}", role)
576 })));
577 }
578
579 let mut organization_id = organization_id;
580 if normalized_role != "superadmin" {
581 if organization_id.is_none() {
582 return Err(HttpResponse::BadRequest().json(json!({
583 "error": format!("Organization is required for role {}", normalized_role)
584 })));
585 }
586 } else {
587 organization_id = None;
588 }
589
590 let is_primary = is_primary.unwrap_or(false);
591 if is_primary {
592 primary_count += 1;
593 if primary_count > 1 {
594 return Err(HttpResponse::BadRequest().json(json!({
595 "error": "Only one primary role can be specified"
596 })));
597 }
598 }
599
600 let key = (normalized_role.clone(), organization_id);
601 if !seen.insert(key) {
602 return Err(HttpResponse::BadRequest().json(json!({
603 "error": "Duplicate role assignment detected"
604 })));
605 }
606
607 normalized.push(NormalizedRoleAssignment {
608 id: Uuid::new_v4(),
609 role: normalized_role,
610 organization_id,
611 is_primary,
612 });
613 }
614
615 if primary_count == 0 {
616 if let Some(first) = normalized.first_mut() {
617 first.is_primary = true;
618 }
619 }
620
621 normalized.sort_by(|a, b| b.is_primary.cmp(&a.is_primary));
622 Ok(normalized)
623}
624
625async fn replace_user_roles(
626 tx: &mut Transaction<'_, Postgres>,
627 user_id: Uuid,
628 roles: &[NormalizedRoleAssignment],
629) -> Result<(), sqlx::Error> {
630 tx.execute(sqlx::query!(
631 "DELETE FROM user_roles WHERE user_id = $1",
632 user_id
633 ))
634 .await?;
635
636 for assignment in roles {
637 tx.execute(sqlx::query!(
638 r#"
639 INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
640 VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
641 "#,
642 assignment.id,
643 user_id,
644 assignment.role,
645 assignment.organization_id,
646 assignment.is_primary
647 ))
648 .await?;
649 }
650
651 Ok(())
652}
653
654async fn load_roles_for_users(
655 pool: &crate::infrastructure::pool::DbPool,
656 user_ids: &[Uuid],
657) -> Result<HashMap<Uuid, Vec<RoleResponse>>, sqlx::Error> {
658 if user_ids.is_empty() {
659 return Ok(HashMap::new());
660 }
661
662 let rows = sqlx::query!(
663 r#"
664 SELECT id, user_id, role, organization_id, is_primary, created_at
665 FROM user_roles
666 WHERE user_id = ANY($1)
667 ORDER BY user_id, is_primary DESC, created_at ASC
668 "#,
669 user_ids
670 )
671 .fetch_all(pool)
672 .await?;
673
674 let mut map: HashMap<Uuid, Vec<RoleResponse>> = HashMap::new();
675
676 for row in rows {
677 let entry = RoleResponse {
678 id: row.id.to_string(),
679 role: row.role,
680 organization_id: row.organization_id.map(|id| id.to_string()),
681 is_primary: row.is_primary,
682 };
683 map.entry(row.user_id).or_default().push(entry);
684 }
685
686 for roles in map.values_mut() {
687 normalize_primary_role(roles);
688 }
689
690 Ok(map)
691}
692
693async fn load_user_response(
694 pool: &crate::infrastructure::pool::DbPool,
695 user_id: Uuid,
696) -> Result<UserResponse, sqlx::Error> {
697 let row = sqlx::query!(
698 r#"
699 SELECT id, email, first_name, last_name, role, organization_id, is_active, created_at
700 FROM users
701 WHERE id = $1
702 "#,
703 user_id
704 )
705 .fetch_one(pool)
706 .await?;
707
708 let roles_map = load_roles_for_users(pool, &[user_id]).await?;
709 let mut roles = roles_map.get(&user_id).cloned().unwrap_or_else(|| {
710 vec![fallback_role_response(
711 row.role.clone(),
712 row.organization_id,
713 )]
714 });
715
716 normalize_primary_role(&mut roles);
717 let active_role = roles
718 .iter()
719 .find(|role| role.is_primary)
720 .cloned()
721 .or_else(|| roles.first().cloned());
722
723 Ok(UserResponse {
724 id: row.id.to_string(),
725 email: row.email,
726 first_name: row.first_name,
727 last_name: row.last_name,
728 role: active_role
729 .as_ref()
730 .map(|r| r.role.clone())
731 .unwrap_or(row.role),
732 organization_id: active_role
733 .as_ref()
734 .and_then(|r| r.organization_id.clone())
735 .or_else(|| row.organization_id.map(|id| id.to_string())),
736 is_active: row.is_active,
737 created_at: row.created_at,
738 roles,
739 active_role,
740 })
741}
742
743fn fallback_role_response(role: String, organization_id: Option<Uuid>) -> RoleResponse {
744 RoleResponse {
745 id: Uuid::new_v4().to_string(),
746 role,
747 organization_id: organization_id.map(|id| id.to_string()),
748 is_primary: true,
749 }
750}
751
752fn normalize_primary_role(roles: &mut [RoleResponse]) {
753 if roles.is_empty() {
754 return;
755 }
756
757 if roles.iter().filter(|r| r.is_primary).count() == 0 {
758 roles[0].is_primary = true;
759 }
760
761 roles.sort_by(|a, b| b.is_primary.cmp(&a.is_primary));
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 use actix_web::http::StatusCode;
768
769 #[test]
770 fn normalize_roles_marks_first_as_primary_when_none_provided() {
771 let primary_org = Uuid::new_v4();
772 let secondary_org = Uuid::new_v4();
773 let input = vec![
774 RoleAssignmentRequest {
775 role: "syndic".to_string(),
776 organization_id: Some(primary_org),
777 is_primary: None,
778 },
779 RoleAssignmentRequest {
780 role: "accountant".to_string(),
781 organization_id: Some(secondary_org),
782 is_primary: Some(false),
783 },
784 ];
785
786 let normalized = normalize_roles(Some(input), None, None).expect("normalized roles");
787 assert_eq!(normalized.len(), 2);
788 assert!(
789 normalized.first().unwrap().is_primary,
790 "first role should become primary"
791 );
792 assert_eq!(
793 normalized.first().unwrap().organization_id,
794 Some(primary_org)
795 );
796 assert_eq!(normalized.first().unwrap().role, "syndic");
797 }
798
799 #[test]
800 fn normalize_roles_rejects_invalid_role() {
801 let res = normalize_roles(
802 Some(vec![RoleAssignmentRequest {
803 role: "invalid-role".to_string(),
804 organization_id: None,
805 is_primary: None,
806 }]),
807 None,
808 None,
809 );
810
811 let err = res.expect_err("invalid role should fail");
812 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
813 }
814
815 #[test]
816 fn normalize_roles_requires_org_for_non_superadmin() {
817 let res = normalize_roles(
818 Some(vec![RoleAssignmentRequest {
819 role: "syndic".to_string(),
820 organization_id: None,
821 is_primary: Some(true),
822 }]),
823 None,
824 None,
825 );
826
827 let err = res.expect_err("organization required");
828 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
829 }
830
831 #[test]
832 fn normalize_roles_uses_fallback_when_no_roles_provided() {
833 let fallback_org = Uuid::new_v4();
834 let roles = normalize_roles(None, Some("syndic".to_string()), Some(fallback_org))
835 .expect("fallback role");
836
837 assert_eq!(roles.len(), 1);
838 let role = roles.first().unwrap();
839 assert_eq!(role.role, "syndic");
840 assert_eq!(role.organization_id, Some(fallback_org));
841 assert!(role.is_primary);
842 }
843}