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
« 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).
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"""
9from __future__ import annotations
11from apps.communications.email_content import Action, Code, Details, EmailContent, Note, Paragraph
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."
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 )
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 )
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 )
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 )
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 )