Coverage for apps/communications/emails.py: 100%

14 statements  

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

1"""The emails the system sends on its own (brand-email-template). 

2 

3Each builder returns ``(subject, EmailContent)``. The wording is unchanged from add-sendgrid-email 

4(Arive's ``auth:reset`` / ``doc:invite`` templates and our invite / code emails); only the structure 

5is new, so the branded layout can show buttons, a details box and a code box. Callers pass the 

6result to ``deliver_email(subject=..., content=...)``; ``manage.py preview_emails`` renders them all. 

7""" 

8 

9from __future__ import annotations 

10 

11from apps.communications.email_content import Action, Code, Details, EmailContent, Note, Paragraph 

12 

13CHANGE_PASSWORD_HINT = ( 

14 "Please change this password after you sign in (My account → Change password)." 

15) 

16NOT_EXPECTING = "If you weren't expecting this email, you can ignore it." 

17 

18 

19def password_reset(company: str, link: str) -> tuple[str, EmailContent]: 

20 """``auth:reset``, for borrowers (portal link) and staff (Staff LOS link).""" 

21 return f"Reset your {company} password", EmailContent.of( 

22 Paragraph("We received a request to reset your password."), 

23 Action("Reset it here (link expires in 1 hour):", link, "Reset password"), 

24 Note("If you didn't request this, you can safely ignore this email."), 

25 ) 

26 

27 

28def sign_in_code(company: str, code: str) -> tuple[str, EmailContent]: 

29 """``auth:otp``.""" 

30 return "Your sign-in code", EmailContent.of( 

31 Code(f"Your {company} sign-in code is", code, "It expires in 5 minutes."), 

32 Note("If you didn't request this, you can safely ignore this email."), 

33 ) 

34 

35 

36def borrower_invite( 

37 *, company: str, name: str, invited_by: str, loan_number: str, login_url: str, email: str, 

38 temp_password: str, 

39) -> tuple[str, EmailContent]: 

40 """``pos:invite``: the borrower portal invitation with the temporary password.""" 

41 return "You're invited to a mortgage application", EmailContent.of( 

42 Paragraph(f"Hi {name},"), 

43 Paragraph( 

44 f"{invited_by} at {company} invited you to the borrower portal for loan " 

45 f"{loan_number}. Sign in to complete your part of the application." 

46 ), 

47 Details((("Email", email), ("Temporary password", temp_password))), 

48 Action("Sign in at:", login_url, "Sign in"), 

49 Paragraph(CHANGE_PASSWORD_HINT), 

50 Note(NOT_EXPECTING), 

51 ) 

52 

53 

54def staff_invite( 

55 *, company: str, first_name: str, role: str, login_url: str, email: str, temp_password: str, 

56) -> tuple[str, EmailContent]: 

57 """``user:invite``: a new staff user's invitation with the temporary password.""" 

58 return f"You're invited to {company} on Lumen", EmailContent.of( 

59 Paragraph(f"Hi {first_name},"), 

60 Paragraph(f"You've been added to {company}'s loan origination workspace as {role}."), 

61 Details((("Email", email), ("Temporary password", temp_password))), 

62 Action("Sign in at:", login_url, "Sign in"), 

63 Paragraph(CHANGE_PASSWORD_HINT), 

64 Note(NOT_EXPECTING), 

65 ) 

66 

67 

68def upload_request( 

69 *, company: str, greeting: str, officer: str, loan_number: str, message: str, link: str, 

70) -> tuple[str, EmailContent]: 

71 """``doc:invite`` (Arive wording), plus the loan officer's note when there is one.""" 

72 return f"Please upload documents for loan {loan_number}", EmailContent.of( 

73 Paragraph(greeting), 

74 Paragraph(f"{officer} at {company} has requested documents for loan {loan_number}."), 

75 Paragraph(message) if message else None, 

76 Action( 

77 "Use your secure upload link to drop them off — no account needed:", link, 

78 "Upload documents", 

79 ), 

80 Paragraph("Thank you."), 

81 )