Coverage for apps/accounts/models.py: 85%
125 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
1"""Organization hierarchy and the custom email-login staff user.
3RBAC models (Role / Permission / License) are deferred to a later sprint; only the
4org hierarchy and the custom user land here so AUTH_USER_MODEL is correct before the
5first migration (design D1 / D2).
6"""
8from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
9from django.db import models
12class OrgUnit(models.Model):
13 """Broker org hierarchy: headquarters and branches."""
15 name = models.CharField(max_length=255)
16 parent = models.ForeignKey(
17 "self", on_delete=models.CASCADE, null=True, blank=True, related_name="sub_units"
18 )
19 nmls_id = models.CharField(max_length=50, blank=True)
20 is_headquarters = models.BooleanField(default=False)
21 created_at = models.DateTimeField(auto_now_add=True)
22 updated_at = models.DateTimeField(auto_now=True)
24 class Meta:
25 db_table = "org_units"
27 def __str__(self) -> str:
28 return self.name
31class UserManager(BaseUserManager):
32 """Manager for the email-login custom user (no username field)."""
34 use_in_migrations = True
36 def _create_user(self, email, password, **extra_fields):
37 if not email: 37 ↛ 38line 37 didn't jump to line 38 because the condition on line 37 was never true
38 raise ValueError("Users must have an email address")
39 email = self.normalize_email(email)
40 user = self.model(email=email, **extra_fields)
41 user.set_password(password)
42 user.save(using=self._db)
43 return user
45 def create_user(self, email, password=None, **extra_fields):
46 extra_fields.setdefault("is_staff", False)
47 extra_fields.setdefault("is_superuser", False)
48 return self._create_user(email, password, **extra_fields)
50 def create_superuser(self, email, password=None, **extra_fields):
51 extra_fields.setdefault("is_staff", True)
52 extra_fields.setdefault("is_superuser", True)
53 if extra_fields.get("is_staff") is not True:
54 raise ValueError("Superuser must have is_staff=True")
55 if extra_fields.get("is_superuser") is not True:
56 raise ValueError("Superuser must have is_superuser=True")
57 return self._create_user(email, password, **extra_fields)
60class User(AbstractBaseUser, PermissionsMixin):
61 """Staff user. Login identifier is email.
63 ``org_unit`` is nullable so a bootstrap superuser can exist before any org is
64 created; application-created staff users are expected to carry an org unit.
65 """
67 org_unit = models.ForeignKey(
68 OrgUnit, on_delete=models.PROTECT, related_name="users", null=True, blank=True
69 )
70 email = models.EmailField(unique=True)
71 # Immutable AWS Cognito subject — the cross-DB link to Tireless (`users.cognito_id`)
72 # and the JIT provisioning / lookup key (nullable: pre-Cognito users backfill on login).
73 cognito_sub = models.CharField(max_length=255, unique=True, null=True, blank=True)
74 first_name = models.CharField(max_length=100)
75 last_name = models.CharField(max_length=100)
76 title = models.CharField(max_length=150, blank=True)
77 phone = models.CharField(max_length=30, blank=True)
78 nmls_id = models.CharField(max_length=50, blank=True)
79 signature = models.CharField(max_length=255, blank=True)
80 notify_email = models.BooleanField(default=True)
81 notify_sms = models.BooleanField(default=False)
82 is_active = models.BooleanField(default=True)
83 is_staff = models.BooleanField(default=False)
84 created_at = models.DateTimeField(auto_now_add=True)
86 objects = UserManager()
88 USERNAME_FIELD = "email"
89 REQUIRED_FIELDS = ["first_name", "last_name"]
91 class Meta:
92 db_table = "users"
94 def __str__(self) -> str:
95 return self.email
98class Permission(models.Model):
99 """A unit of access, referenced by stable code (e.g. "LOAN_EDIT")."""
101 code = models.CharField(max_length=64, unique=True)
102 label = models.CharField(max_length=128, blank=True)
103 branch_override = models.BooleanField(default=True)
104 admin_role_check_reqd = models.BooleanField(default=False)
106 class Meta:
107 db_table = "permissions"
109 def __str__(self) -> str:
110 return self.code
113class Role(models.Model):
114 """A named bundle of permissions (e.g. "Loan Officer")."""
116 name = models.CharField(max_length=64, unique=True)
118 class Meta:
119 db_table = "roles"
121 def __str__(self) -> str:
122 return self.name
125class RolePermission(models.Model):
126 """Role → permission matrix entry."""
128 role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="role_permissions")
129 permission = models.ForeignKey(
130 Permission, on_delete=models.CASCADE, related_name="role_permissions"
131 )
133 class Meta:
134 db_table = "role_permissions"
135 unique_together = ("role", "permission")
138class UserRole(models.Model):
139 """A user's role within an org unit (null org unit = org-wide / HQ)."""
141 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_roles")
142 role = models.ForeignKey(Role, on_delete=models.PROTECT, related_name="user_roles")
143 org_unit = models.ForeignKey(
144 OrgUnit, on_delete=models.CASCADE, null=True, blank=True, related_name="user_roles"
145 )
147 class Meta:
148 db_table = "user_roles"
149 unique_together = ("user", "role", "org_unit")
152class License(models.Model):
153 """NMLS + per-state licensing for a user."""
155 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="licenses")
156 state_code = models.CharField(max_length=2)
157 nmls_id = models.CharField(max_length=50, blank=True)
158 active = models.BooleanField(default=True)
159 sponsored = models.BooleanField(default=False)
161 class Meta:
162 db_table = "licenses"
163 unique_together = ("user", "state_code")
165 def __str__(self) -> str:
166 return f"{self.user_id}:{self.state_code}"
169class Company(models.Model):
170 """The one company this deployment serves (add-admin-organization-api) — a true
171 singleton, same lazy-create-on-first-read pattern as ``core_los.LoanNumberSequence``."""
173 name = models.CharField(max_length=255, blank=True)
174 nmls_id = models.CharField(max_length=50, blank=True)
175 type = models.CharField(max_length=50, blank=True)
176 dba = models.CharField(max_length=255, blank=True)
177 phone = models.CharField(max_length=30, blank=True)
178 mers_id = models.CharField(max_length=50, blank=True)
179 address_line = models.CharField(max_length=255, blank=True)
180 city = models.CharField(max_length=100, blank=True)
181 state_code = models.CharField(max_length=2, blank=True)
182 postal_code = models.CharField(max_length=10, blank=True)
183 business_hours = models.CharField(max_length=255, blank=True)
184 pos_config = models.JSONField(default=dict, blank=True)
185 lead_form_config = models.JSONField(default=dict, blank=True)
186 agent_config = models.JSONField(default=dict, blank=True)
187 non_delegated_config = models.JSONField(default=dict, blank=True)
189 class Meta:
190 db_table = "company"
192 def __str__(self) -> str:
193 return self.name or "Company"
196class ApiClient(models.Model):
197 """Partner OAuth client / API key for external automation (e.g. Zapier)."""
199 client_id = models.CharField(max_length=100, unique=True)
200 org_unit = models.ForeignKey(OrgUnit, on_delete=models.CASCADE, null=True, blank=True)
201 label = models.CharField(max_length=255, blank=True)
202 secret_enc = models.TextField(blank=True)
203 api_key_enc = models.TextField(blank=True)
204 created_at = models.DateTimeField(auto_now_add=True)
206 class Meta:
207 db_table = "api_clients"