feat(emails): subtask 3 — 4 medium templates (quote_verification, waitlist, lead_matched)

- Add quote_verification.html (with optional project recap card)
- Add waitlist_supplier.html, waitlist_general.html
- Add lead_matched.html (with next-steps section + tip box)
- Refactor 3 handlers in worker.py: send_quote_verification,
  send_waitlist_confirmation, send_lead_matched_notification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-25 12:05:20 +01:00
parent daf1945d5b
commit 1c7cdc42f2
5 changed files with 100 additions and 66 deletions

View File

@@ -0,0 +1,17 @@
{% extends "emails/_base.html" %}
{% from "emails/_macros.html" import email_button %}
{% block body %}
<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{{ t.email_lead_matched_heading }}</h2>
<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">
<p>{{ t.email_lead_matched_greeting | tformat(first_name=first_name) }}</p>
<p>{{ t.email_lead_matched_body }}</p>
<p style="font-size:13px;color:#64748B;">{{ t.email_lead_matched_context | tformat(facility_type=facility_type, court_count=court_count, country=country) }}</p>
<p style="font-size:14px;font-weight:600;color:#0F172A;margin:20px 0 8px;">{{ t.email_lead_matched_next_heading }}</p>
<p style="font-size:14px;color:#334155;">{{ t.email_lead_matched_next_body }}</p>
<p style="font-size:13px;color:#64748B;padding:10px 14px;background-color:#F0F9FF;border-radius:6px;border-left:3px solid #1D4ED8;">{{ t.email_lead_matched_tip }}</p>
{{ email_button(base_url ~ "/dashboard", t.email_lead_matched_btn) }}
<p style="font-size:12px;color:#94A3B8;">{{ t.email_lead_matched_note }}</p>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "emails/_base.html" %}
{% from "emails/_macros.html" import email_button %}
{% block body %}
<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{{ t.email_quote_verify_heading }}</h2>
<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">
<p>{{ t.email_quote_verify_greeting | tformat(first_name=first_name) }}</p>
<p>{{ t.email_quote_verify_body | tformat(app_name=app_name) }}</p>
{%- if recap_parts %}
<table cellpadding="0" cellspacing="0" width="100%" style="margin:16px 0;border:1px solid #E2E8F0;border-radius:8px;overflow:hidden;">
<tr><td style="padding:14px 18px;background-color:#F8FAFC;font-size:13px;color:#64748B;">
<strong style="color:#0F172A;">{{ t.email_quote_verify_project_label }}</strong> {{ recap_parts | join(" &middot; ") | safe }}
</td></tr>
</table>
{%- endif %}
<p style="font-size:13px;color:#334155;">{{ t.email_quote_verify_urgency }}</p>
{{ email_button(link, t.email_quote_verify_btn) }}
<p style="font-size:13px;color:#94A3B8;">{{ t.email_quote_verify_expires }}</p>
<p style="font-size:13px;color:#94A3B8;">{{ t.email_quote_verify_fallback }}</p>
<p style="font-size:13px;color:#94A3B8;word-break:break-all;">{{ link }}</p>
<p style="font-size:13px;color:#94A3B8;">{{ t.email_quote_verify_ignore }}</p>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "emails/_base.html" %}
{% block body %}
<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{{ t.email_waitlist_general_heading }}</h2>
<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">
<p>{{ t.email_waitlist_general_body }}</p>
<p>{{ t.email_waitlist_general_perks_intro }}</p>
<ul style="font-size:14px;color:#1E293B;margin:16px 0;">
<li>{{ t.email_waitlist_general_perk_1 }}</li>
<li>{{ t.email_waitlist_general_perk_2 }}</li>
<li>{{ t.email_waitlist_general_perk_3 }}</li>
</ul>
<p style="font-size:13px;color:#64748B;">{{ t.email_waitlist_general_outro }}</p>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "emails/_base.html" %}
{% block body %}
<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{{ t.email_waitlist_supplier_heading }}</h2>
<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">
<p>{{ t.email_waitlist_supplier_body | tformat(plan_name=plan_name) }}</p>
<p>{{ t.email_waitlist_supplier_perks_intro }}</p>
<ul style="font-size:14px;color:#1E293B;margin:16px 0;">
<li>{{ t.email_waitlist_supplier_perk_1 }}</li>
<li>{{ t.email_waitlist_supplier_perk_2 }}</li>
<li>{{ t.email_waitlist_supplier_perk_3 }}</li>
</ul>
<p style="font-size:13px;color:#64748B;">{{ t.email_waitlist_supplier_meanwhile }}</p>
<ul style="font-size:13px;color:#64748B;">
<li><a href="{{ base_url }}/planner" style="color:#1D4ED8;">{{ t.email_waitlist_supplier_link_planner }}</a></li>
<li><a href="{{ base_url }}/directory" style="color:#1D4ED8;">{{ t.email_waitlist_supplier_link_directory }}</a></li>
</ul>
{% endblock %}

View File

@@ -265,8 +265,6 @@ async def handle_send_quote_verification(payload: dict) -> None:
facility_type = payload.get("facility_type", "")
country = payload.get("country", "")
# Project recap card
project_card = ""
recap_parts = []
if court_count:
recap_parts.append(f"{court_count} courts")
@@ -274,34 +272,22 @@ async def handle_send_quote_verification(payload: dict) -> None:
recap_parts.append(facility_type)
if country:
recap_parts.append(country)
if recap_parts:
project_card = (
f'<table cellpadding="0" cellspacing="0" width="100%" style="margin:16px 0;border:1px solid #E2E8F0;border-radius:8px;overflow:hidden;">'
f'<tr><td style="padding:14px 18px;background-color:#F8FAFC;font-size:13px;color:#64748B;">'
f'<strong style="color:#0F172A;">{_t("email_quote_verify_project_label", lang)}</strong> {" &middot; ".join(recap_parts)}'
f'</td></tr></table>'
)
preheader = _t("email_quote_verify_preheader_courts", lang, court_count=court_count) if court_count else _t("email_quote_verify_preheader", lang)
body = (
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_quote_verify_heading", lang)}</h2>'
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
f'<p>{_t("email_quote_verify_greeting", lang, first_name=first_name)}</p>'
f'<p>{_t("email_quote_verify_body", lang, app_name=config.APP_NAME)}</p>'
f'{project_card}'
f'<p style="font-size:13px;color:#334155;">{_t("email_quote_verify_urgency", lang)}</p>'
f'{_email_button(link, _t("email_quote_verify_btn", lang))}'
f'<p style="font-size:13px;color:#94A3B8;">{_t("email_quote_verify_expires", lang)}</p>'
f'<p style="font-size:13px;color:#94A3B8;">{_t("email_quote_verify_fallback", lang)}</p>'
f'<p style="font-size:13px;color:#94A3B8;word-break:break-all;">{link}</p>'
f'<p style="font-size:13px;color:#94A3B8;">{_t("email_quote_verify_ignore", lang)}</p>'
html = render_email_template(
"emails/quote_verification.html",
lang=lang,
link=link,
first_name=first_name,
recap_parts=recap_parts,
preheader=preheader,
)
await send_email(
to=payload["email"],
subject=_t("email_quote_verify_subject", lang),
html=_email_wrap(body, lang, preheader=preheader),
html=html,
from_addr=EMAIL_ADDRESSES["transactional"],
email_type="quote_verification",
)
@@ -340,43 +326,24 @@ async def handle_send_waitlist_confirmation(payload: dict) -> None:
if intent.startswith("supplier_"):
plan_name = intent.replace("supplier_", "").title()
subject = _t("email_waitlist_supplier_subject", lang, plan_name=plan_name)
preheader = _t("email_waitlist_supplier_preheader", lang)
body = (
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_waitlist_supplier_heading", lang)}</h2>'
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
f'<p>{_t("email_waitlist_supplier_body", lang, plan_name=plan_name)}</p>'
f'<p>{_t("email_waitlist_supplier_perks_intro", lang)}</p>'
f'<ul style="font-size:14px;color:#1E293B;margin:16px 0;">'
f'<li>{_t("email_waitlist_supplier_perk_1", lang)}</li>'
f'<li>{_t("email_waitlist_supplier_perk_2", lang)}</li>'
f'<li>{_t("email_waitlist_supplier_perk_3", lang)}</li>'
f'</ul>'
f'<p style="font-size:13px;color:#64748B;">{_t("email_waitlist_supplier_meanwhile", lang)}</p>'
f'<ul style="font-size:13px;color:#64748B;">'
f'<li><a href="{config.BASE_URL}/planner" style="color:#1D4ED8;">{_t("email_waitlist_supplier_link_planner", lang)}</a></li>'
f'<li><a href="{config.BASE_URL}/directory" style="color:#1D4ED8;">{_t("email_waitlist_supplier_link_directory", lang)}</a></li>'
f'</ul>'
html = render_email_template(
"emails/waitlist_supplier.html",
lang=lang,
plan_name=plan_name,
preheader=_t("email_waitlist_supplier_preheader", lang),
)
else:
subject = _t("email_waitlist_general_subject", lang)
preheader = _t("email_waitlist_general_preheader", lang)
body = (
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_waitlist_general_heading", lang)}</h2>'
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
f'<p>{_t("email_waitlist_general_body", lang)}</p>'
f'<p>{_t("email_waitlist_general_perks_intro", lang)}</p>'
f'<ul style="font-size:14px;color:#1E293B;margin:16px 0;">'
f'<li>{_t("email_waitlist_general_perk_1", lang)}</li>'
f'<li>{_t("email_waitlist_general_perk_2", lang)}</li>'
f'<li>{_t("email_waitlist_general_perk_3", lang)}</li>'
f'</ul>'
f'<p style="font-size:13px;color:#64748B;">{_t("email_waitlist_general_outro", lang)}</p>'
html = render_email_template(
"emails/waitlist_general.html",
lang=lang,
preheader=_t("email_waitlist_general_preheader", lang),
)
await send_email(
to=email,
subject=subject,
html=_email_wrap(body, lang, preheader=preheader),
html=html,
from_addr=EMAIL_ADDRESSES["transactional"],
email_type="waitlist",
)
@@ -524,26 +491,20 @@ async def handle_send_lead_matched_notification(payload: dict) -> None:
first_name = (lead["contact_name"] or "").split()[0] if lead.get("contact_name") else "there"
body = (
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_lead_matched_heading", lang)}</h2>'
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
f'<p>{_t("email_lead_matched_greeting", lang, first_name=first_name)}</p>'
f'<p>{_t("email_lead_matched_body", lang)}</p>'
f'<p style="font-size:13px;color:#64748B;">{_t("email_lead_matched_context", lang, facility_type=lead["facility_type"] or "padel", court_count=lead["court_count"] or "?", country=lead["country"] or "your area")}</p>'
# What happens next
f'<p style="font-size:14px;font-weight:600;color:#0F172A;margin:20px 0 8px;">{_t("email_lead_matched_next_heading", lang)}</p>'
f'<p style="font-size:14px;color:#334155;">{_t("email_lead_matched_next_body", lang)}</p>'
f'<p style="font-size:13px;color:#64748B;padding:10px 14px;background-color:#F0F9FF;'
f'border-radius:6px;border-left:3px solid #1D4ED8;">'
f'{_t("email_lead_matched_tip", lang)}</p>'
f'{_email_button(f"{config.BASE_URL}/dashboard", _t("email_lead_matched_btn", lang))}'
f'<p style="font-size:12px;color:#94A3B8;">{_t("email_lead_matched_note", lang)}</p>'
html = render_email_template(
"emails/lead_matched.html",
lang=lang,
first_name=first_name,
facility_type=lead["facility_type"] or "padel",
court_count=lead["court_count"] or "?",
country=lead["country"] or "your area",
preheader=_t("email_lead_matched_preheader", lang),
)
await send_email(
to=lead["contact_email"],
subject=_t("email_lead_matched_subject", lang, first_name=first_name),
html=_email_wrap(body, lang, preheader=_t("email_lead_matched_preheader", lang)),
html=html,
from_addr=EMAIL_ADDRESSES["leads"],
email_type="lead_matched",
)