Coverage for apps/accounts/setup_api.py: 76%
75 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"""First-run setup installer router (mounted at /api/setup).
3Endpoints:
4 GET /api/setup/status -> SetupStatusOut (public)
5 POST /api/setup -> 201 SetupResultOut (public; signs new Owner in)
6"""
8from __future__ import annotations
10from django.core.management import call_command
11from django.db import transaction
12from ninja import Router
14from apps.accounts.models import Company, License, OrgUnit, Role, User, UserRole
15from apps.accounts.setup_schemas import SetupInput, SetupResultOut, SetupStatusOut
16from apps.core_los.models import Loan
17from domain.result import AppError, DomainError
19router = Router(tags=["installer-setup"])
22@router.get("/status", response=SetupStatusOut)
23def get_setup_status(request):
24 """Return whether initial company setup has been performed."""
25 company = Company.objects.first()
26 has_company = company is not None
27 user_count = User.objects.count()
28 return {
29 "provisioned": has_company,
30 "company": company.name if company else None,
31 "user_count": user_count,
32 }
35@router.post("", response={201: SetupResultOut})
36def run_setup(request, payload: SetupInput):
37 """First-run installer runner: provision company, branches, Owner and sign in."""
38 admin_info = payload.admin
39 if ( 39 ↛ 44line 39 didn't jump to line 44 because the condition on line 39 was never true
40 not admin_info.name.strip()
41 or not admin_info.email.strip()
42 or not admin_info.password.strip()
43 ):
44 raise DomainError(
45 [AppError("VALIDATION", "An admin name, email and password are required.")]
46 )
48 if len(admin_info.password.strip()) < 4:
49 raise DomainError(
50 [AppError("VALIDATION", "New password must be at least 4 characters.")]
51 )
53 # Ensure RBAC roles exist
54 call_command("seed_rbac")
55 owner_role = Role.objects.filter(name="Owner").first()
56 lo_role = Role.objects.filter(name="Loan Officer").first()
58 with transaction.atomic():
59 # 1. Company
60 company_data = payload.company
61 company = Company.objects.first()
62 if company is None: 62 ↛ 69line 62 didn't jump to line 69 because the condition on line 62 was always true
63 company = Company.objects.create(
64 name=company_data.name,
65 nmls_id=company_data.nmls_id,
66 type=company_data.type,
67 )
68 else:
69 company.name = company_data.name
70 company.nmls_id = company_data.nmls_id
71 company.type = company_data.type
72 company.save()
74 # 2. HQ OrgUnit
75 hq = OrgUnit.objects.filter(is_headquarters=True).first()
76 if hq is None: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 hq = OrgUnit.objects.create(
78 name=company_data.name or "Headquarters",
79 nmls_id=company_data.nmls_id,
80 is_headquarters=True,
81 )
82 else:
83 hq.name = company_data.name or hq.name
84 hq.nmls_id = company_data.nmls_id or hq.nmls_id
85 hq.save()
87 # 3. Branches
88 for b in payload.branches:
89 OrgUnit.objects.get_or_create(
90 name=b.label,
91 parent=hq,
92 defaults={"nmls_id": company_data.nmls_id, "is_headquarters": False},
93 )
95 # 4. Owner Admin User
96 admin_name_parts = admin_info.name.strip().split(" ", 1)
97 first_name = admin_name_parts[0]
98 last_name = admin_name_parts[1] if len(admin_name_parts) > 1 else ""
100 admin_user = User.objects.filter(email=admin_info.email.strip().lower()).first()
101 if admin_user is None: 101 ↛ 110line 101 didn't jump to line 110 because the condition on line 101 was always true
102 admin_user = User.objects.create_user(
103 email=admin_info.email.strip().lower(),
104 password=admin_info.password,
105 first_name=first_name,
106 last_name=last_name,
107 org_unit=hq,
108 )
109 else:
110 admin_user.set_password(admin_info.password)
111 admin_user.first_name = first_name
112 admin_user.last_name = last_name
113 admin_user.org_unit = hq
114 admin_user.is_active = True
115 admin_user.save()
117 if owner_role: 117 ↛ 121line 117 didn't jump to line 121 because the condition on line 117 was always true
118 UserRole.objects.get_or_create(user=admin_user, role=owner_role, org_unit=hq)
120 # 5. Optional Loan Officer
121 lo_info = payload.loan_officer
122 if lo_info and lo_info.email.strip(): 122 ↛ 153line 122 didn't jump to line 153 because the condition on line 122 was always true
123 lo_parts = lo_info.name.strip().split(" ", 1)
124 lo_first = lo_parts[0]
125 lo_last = lo_parts[1] if len(lo_parts) > 1 else ""
127 lo_user = User.objects.filter(email=lo_info.email.strip().lower()).first()
128 if lo_user is None: 128 ↛ 138line 128 didn't jump to line 138 because the condition on line 128 was always true
129 lo_user = User.objects.create_user(
130 email=lo_info.email.strip().lower(),
131 password=lo_info.password or "lumen",
132 first_name=lo_first,
133 last_name=lo_last,
134 org_unit=hq,
135 nmls_id=lo_info.nmls_id,
136 )
137 else:
138 lo_user.org_unit = hq
139 lo_user.nmls_id = lo_info.nmls_id or lo_user.nmls_id
140 lo_user.save()
142 if lo_role: 142 ↛ 145line 142 didn't jump to line 145 because the condition on line 142 was always true
143 UserRole.objects.get_or_create(user=lo_user, role=lo_role, org_unit=hq)
145 for lic in lo_info.licenses:
146 License.objects.update_or_create(
147 user=lo_user,
148 state_code=lic.state_code.upper(),
149 defaults={"active": True, "sponsored": lic.sponsored},
150 )
152 # 6. Clear loans if requested
153 if payload.clear_loans: 153 ↛ 156line 153 didn't jump to line 156
154 Loan.objects.all().delete()
156 token = f"staff:{admin_user.email}"
157 return 201, {
158 "token": token,
159 "user": {
160 "id": admin_user.id,
161 "name": admin_info.name,
162 "role": "Owner",
163 },
164 }