Coverage for apps/accounts/setup_api.py: 76%

75 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-04 14:06 +0000

1"""First-run setup installer router (mounted at /api/setup). 

2 

3Endpoints: 

4 GET /api/setup/status -> SetupStatusOut (public) 

5 POST /api/setup -> 201 SetupResultOut (public; signs new Owner in) 

6""" 

7 

8from __future__ import annotations 

9 

10from django.core.management import call_command 

11from django.db import transaction 

12from ninja import Router 

13 

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 

18 

19router = Router(tags=["installer-setup"]) 

20 

21 

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 } 

33 

34 

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 ) 

47 

48 if len(admin_info.password.strip()) < 4: 

49 raise DomainError( 

50 [AppError("VALIDATION", "New password must be at least 4 characters.")] 

51 ) 

52 

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() 

57 

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() 

73 

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() 

86 

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 ) 

94 

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 "" 

99 

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() 

116 

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) 

119 

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 "" 

126 

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() 

141 

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) 

144 

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 ) 

151 

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() 

155 

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 }