Coverage for apps/borrower_portal/account_flows.py: 94%

49 statements  

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

1"""Borrower branches of the shared account endpoints (``/auth/me``, ``/me/profile``, 

2``/me/password``), called from ``apps.accounts.local_auth_api`` when the caller is a borrower 

3(add-borrower-portal-api design.md D3, D5).""" 

4 

5from __future__ import annotations 

6 

7from django.db import transaction 

8 

9from apps.application_1003.models import Borrower 

10from apps.borrower_portal.models import BorrowerAccount 

11from apps.borrower_portal.services import account_for_principal, borrower_user 

12from domain.pos import first_error, validate_new_password, validate_profile 

13from domain.result import AppError, DomainError 

14 

15 

16def current_account(request) -> BorrowerAccount: 

17 account = account_for_principal(request.principal) 

18 if account is None: 18 ↛ 19line 18 didn't jump to line 19 because the condition on line 18 was never true

19 raise DomainError([AppError("UNAUTHORIZED", "Not signed in.")]) 

20 return account 

21 

22 

23def me(request) -> dict: 

24 return borrower_user(current_account(request)) 

25 

26 

27def update_profile(request, data: dict) -> dict: 

28 """``PATCH /api/me/profile`` for a borrower. Fields not sent keep their saved values.""" 

29 account = current_account(request) 

30 notify = data.get("notify") or {} 

31 name = data.get("name", account.name) 

32 email = data.get("email", account.email) 

33 phone = data.get("phone", account.phone) 

34 name, email, phone = (name or "").strip(), (email or "").strip(), (phone or "").strip() 

35 

36 problem = first_error(validate_profile(name, email, phone)) 

37 if problem: 

38 raise DomainError([problem]) 

39 new_email = email.lower() 

40 if ( 

41 new_email != account.email 

42 and BorrowerAccount.objects.exclude(pk=account.pk).filter(email=new_email).exists() 

43 ): 

44 raise DomainError([AppError("VALIDATION", "That email is already in use.", "email")]) 

45 

46 with transaction.atomic(): 

47 old_email = account.email 

48 account.name = name 

49 account.email = new_email 

50 account.phone = phone 

51 if "email" in notify: 51 ↛ 53line 51 didn't jump to line 53 because the condition on line 51 was always true

52 account.notify_email = bool(notify["email"]) 

53 if "sms" in notify: 53 ↛ 55line 53 didn't jump to line 55 because the condition on line 53 was always true

54 account.notify_sms = bool(notify["sms"]) 

55 account.save() 

56 if new_email != old_email: 

57 # Loans are linked by Borrower.email (build_borrower_principal): move the borrower's 

58 # own rows with the account so they keep every loan they had. 

59 Borrower.objects.filter( 

60 loan_id__in=request.principal.loan_ids, email__iexact=old_email 

61 ).update(email=new_email) 

62 return borrower_user(account) 

63 

64 

65def change_password(request, current: str, new: str) -> None: 

66 """``POST /api/me/password`` for a borrower. A wrong current password is AUTH_BAD_PASSWORD, 

67 which the error mapper answers with 422 — never 401, so the portal does not sign out.""" 

68 account = current_account(request) 

69 if not account.check_password(current): 

70 raise DomainError( 

71 [AppError("AUTH_BAD_PASSWORD", "Current password is incorrect.", "current")] 

72 ) 

73 problem = first_error(validate_new_password(current, new)) 

74 if problem: 

75 raise DomainError([problem]) 

76 account.set_password(new) 

77 account.save(update_fields=["password", "updated_at"])