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
« 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)."""
5from __future__ import annotations
7from django.db import transaction
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
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
23def me(request) -> dict:
24 return borrower_user(current_account(request))
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()
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")])
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)
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"])