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:
17
web/src/padelnomics/templates/emails/lead_matched.html
Normal file
17
web/src/padelnomics/templates/emails/lead_matched.html
Normal 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 %}
|
||||||
24
web/src/padelnomics/templates/emails/quote_verification.html
Normal file
24
web/src/padelnomics/templates/emails/quote_verification.html
Normal 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(" · ") | 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 %}
|
||||||
14
web/src/padelnomics/templates/emails/waitlist_general.html
Normal file
14
web/src/padelnomics/templates/emails/waitlist_general.html
Normal 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 %}
|
||||||
18
web/src/padelnomics/templates/emails/waitlist_supplier.html
Normal file
18
web/src/padelnomics/templates/emails/waitlist_supplier.html
Normal 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 %}
|
||||||
@@ -265,8 +265,6 @@ async def handle_send_quote_verification(payload: dict) -> None:
|
|||||||
facility_type = payload.get("facility_type", "")
|
facility_type = payload.get("facility_type", "")
|
||||||
country = payload.get("country", "")
|
country = payload.get("country", "")
|
||||||
|
|
||||||
# Project recap card
|
|
||||||
project_card = ""
|
|
||||||
recap_parts = []
|
recap_parts = []
|
||||||
if court_count:
|
if court_count:
|
||||||
recap_parts.append(f"{court_count} courts")
|
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)
|
recap_parts.append(facility_type)
|
||||||
if country:
|
if country:
|
||||||
recap_parts.append(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> {" · ".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)
|
preheader = _t("email_quote_verify_preheader_courts", lang, court_count=court_count) if court_count else _t("email_quote_verify_preheader", lang)
|
||||||
|
|
||||||
body = (
|
html = render_email_template(
|
||||||
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_quote_verify_heading", lang)}</h2>'
|
"emails/quote_verification.html",
|
||||||
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
|
lang=lang,
|
||||||
f'<p>{_t("email_quote_verify_greeting", lang, first_name=first_name)}</p>'
|
link=link,
|
||||||
f'<p>{_t("email_quote_verify_body", lang, app_name=config.APP_NAME)}</p>'
|
first_name=first_name,
|
||||||
f'{project_card}'
|
recap_parts=recap_parts,
|
||||||
f'<p style="font-size:13px;color:#334155;">{_t("email_quote_verify_urgency", lang)}</p>'
|
preheader=preheader,
|
||||||
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>'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await send_email(
|
await send_email(
|
||||||
to=payload["email"],
|
to=payload["email"],
|
||||||
subject=_t("email_quote_verify_subject", lang),
|
subject=_t("email_quote_verify_subject", lang),
|
||||||
html=_email_wrap(body, lang, preheader=preheader),
|
html=html,
|
||||||
from_addr=EMAIL_ADDRESSES["transactional"],
|
from_addr=EMAIL_ADDRESSES["transactional"],
|
||||||
email_type="quote_verification",
|
email_type="quote_verification",
|
||||||
)
|
)
|
||||||
@@ -340,43 +326,24 @@ async def handle_send_waitlist_confirmation(payload: dict) -> None:
|
|||||||
if intent.startswith("supplier_"):
|
if intent.startswith("supplier_"):
|
||||||
plan_name = intent.replace("supplier_", "").title()
|
plan_name = intent.replace("supplier_", "").title()
|
||||||
subject = _t("email_waitlist_supplier_subject", lang, plan_name=plan_name)
|
subject = _t("email_waitlist_supplier_subject", lang, plan_name=plan_name)
|
||||||
preheader = _t("email_waitlist_supplier_preheader", lang)
|
html = render_email_template(
|
||||||
body = (
|
"emails/waitlist_supplier.html",
|
||||||
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_waitlist_supplier_heading", lang)}</h2>'
|
lang=lang,
|
||||||
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
|
plan_name=plan_name,
|
||||||
f'<p>{_t("email_waitlist_supplier_body", lang, plan_name=plan_name)}</p>'
|
preheader=_t("email_waitlist_supplier_preheader", lang),
|
||||||
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>'
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
subject = _t("email_waitlist_general_subject", lang)
|
subject = _t("email_waitlist_general_subject", lang)
|
||||||
preheader = _t("email_waitlist_general_preheader", lang)
|
html = render_email_template(
|
||||||
body = (
|
"emails/waitlist_general.html",
|
||||||
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_waitlist_general_heading", lang)}</h2>'
|
lang=lang,
|
||||||
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
|
preheader=_t("email_waitlist_general_preheader", lang),
|
||||||
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>'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await send_email(
|
await send_email(
|
||||||
to=email,
|
to=email,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
html=_email_wrap(body, lang, preheader=preheader),
|
html=html,
|
||||||
from_addr=EMAIL_ADDRESSES["transactional"],
|
from_addr=EMAIL_ADDRESSES["transactional"],
|
||||||
email_type="waitlist",
|
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"
|
first_name = (lead["contact_name"] or "").split()[0] if lead.get("contact_name") else "there"
|
||||||
|
|
||||||
body = (
|
html = render_email_template(
|
||||||
f'<h2 style="margin:0 0 8px;color:#0F172A;font-size:22px;">{_t("email_lead_matched_heading", lang)}</h2>'
|
"emails/lead_matched.html",
|
||||||
f'<hr style="border:none;border-top:1px solid #E2E8F0;margin:0 0 16px;">'
|
lang=lang,
|
||||||
f'<p>{_t("email_lead_matched_greeting", lang, first_name=first_name)}</p>'
|
first_name=first_name,
|
||||||
f'<p>{_t("email_lead_matched_body", lang)}</p>'
|
facility_type=lead["facility_type"] or "padel",
|
||||||
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>'
|
court_count=lead["court_count"] or "?",
|
||||||
# What happens next
|
country=lead["country"] or "your area",
|
||||||
f'<p style="font-size:14px;font-weight:600;color:#0F172A;margin:20px 0 8px;">{_t("email_lead_matched_next_heading", lang)}</p>'
|
preheader=_t("email_lead_matched_preheader", lang),
|
||||||
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>'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await send_email(
|
await send_email(
|
||||||
to=lead["contact_email"],
|
to=lead["contact_email"],
|
||||||
subject=_t("email_lead_matched_subject", lang, first_name=first_name),
|
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"],
|
from_addr=EMAIL_ADDRESSES["leads"],
|
||||||
email_type="lead_matched",
|
email_type="lead_matched",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user