feat: add survey invite email template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -283,6 +283,144 @@ class EmailService:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def send_survey_notification_email(
|
||||||
|
to_email: str,
|
||||||
|
respondent_name: str | None,
|
||||||
|
responses: dict,
|
||||||
|
) -> bool:
|
||||||
|
"""Send survey response notification. Fire-and-forget."""
|
||||||
|
if not settings.email_enabled:
|
||||||
|
logger.warning("Email not sent — RESEND_API_KEY not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import resend
|
||||||
|
import html as html_mod
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
resend.api_key = settings.RESEND_API_KEY
|
||||||
|
|
||||||
|
name_display = html_mod.escape(respondent_name) if respondent_name else "Anonymous"
|
||||||
|
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
subject = f"[FlowPilot Survey] New Response from {name_display} — {date_str}"
|
||||||
|
|
||||||
|
rows_html = ""
|
||||||
|
for q_id, answer in responses.items():
|
||||||
|
safe_id = html_mod.escape(str(q_id))
|
||||||
|
if isinstance(answer, list):
|
||||||
|
answer_display = "<br>".join(f"• {html_mod.escape(str(item))}" for item in answer)
|
||||||
|
else:
|
||||||
|
answer_display = html_mod.escape(str(answer))
|
||||||
|
rows_html += f"""
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px 14px; border-bottom: 1px solid rgba(255,255,255,0.06); color: #06b6d4; font-weight: 600; vertical-align: top; width: 120px; font-size: 13px;">{safe_id}</td>
|
||||||
|
<td style="padding: 10px 14px; border-bottom: 1px solid rgba(255,255,255,0.06); color: #e4e4e7; font-size: 14px; white-space: pre-wrap;">{answer_display}</td>
|
||||||
|
</tr>"""
|
||||||
|
|
||||||
|
email_html = f"""<!DOCTYPE html>
|
||||||
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
||||||
|
<body style="margin:0; padding:0; background-color:#101114; font-family:Arial,sans-serif;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#101114; padding:40px 20px;">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#14161a; border-radius:16px; border:1px solid rgba(255,255,255,0.06);">
|
||||||
|
<tr><td style="padding:32px 32px 20px;">
|
||||||
|
<h1 style="margin:0 0 4px; font-size:20px; color:#f8fafc;">FlowPilot Survey Response</h1>
|
||||||
|
<p style="margin:0; font-size:13px; color:#5a6170;">From: {name_display} • {date_str}</p>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 32px 32px;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid rgba(255,255,255,0.06); border-radius:8px; overflow:hidden;">
|
||||||
|
{rows_html}
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 32px 24px;">
|
||||||
|
<p style="margin:0; font-size:12px; color:#5a6170;">This response has been saved to the survey_responses table.</p>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
resend.Emails.send({
|
||||||
|
"from": settings.FROM_EMAIL,
|
||||||
|
"to": [to_email],
|
||||||
|
"subject": subject,
|
||||||
|
"html": email_html,
|
||||||
|
})
|
||||||
|
logger.info("Survey notification sent for respondent: %s", name_display)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to send survey notification email")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def send_survey_invite_email(
|
||||||
|
to_email: str,
|
||||||
|
recipient_name: str,
|
||||||
|
survey_url: str,
|
||||||
|
) -> bool:
|
||||||
|
"""Send survey invite email."""
|
||||||
|
if not settings.email_enabled:
|
||||||
|
logger.warning("Email not sent — RESEND_API_KEY not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import resend
|
||||||
|
import html as html_mod
|
||||||
|
|
||||||
|
resend.api_key = settings.RESEND_API_KEY
|
||||||
|
|
||||||
|
safe_name = html_mod.escape(recipient_name)
|
||||||
|
subject = "FlowPilot Survey — Your expertise matters"
|
||||||
|
|
||||||
|
email_html = f"""<!DOCTYPE html>
|
||||||
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
||||||
|
<body style="margin:0;padding:0;background:#101114;font-family:'Inter',Helvetica,Arial,sans-serif;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#101114;padding:40px 0;">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#14161a;border:1px solid rgba(255,255,255,0.06);border-radius:16px;">
|
||||||
|
<tr><td style="padding:40px 40px 24px;text-align:center;">
|
||||||
|
<h1 style="margin:0;color:#f8fafc;font-size:24px;font-weight:600;">Resolution<span style="color:#06b6d4;">Flow</span></h1>
|
||||||
|
<p style="margin:8px 0 0;color:#5a6170;font-size:14px;">FlowPilot Research</p>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 40px 24px;">
|
||||||
|
<p style="margin:0;color:#8891a0;font-size:16px;line-height:1.6;">
|
||||||
|
Hi {safe_name},
|
||||||
|
</p>
|
||||||
|
<p style="margin:12px 0 0;color:#8891a0;font-size:16px;line-height:1.6;">
|
||||||
|
We're building an AI assistant for MSP engineers and would love your input. This 5-minute survey will help shape how FlowPilot thinks about troubleshooting.
|
||||||
|
</p>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 40px 32px;text-align:center;">
|
||||||
|
<a href="{survey_url}" style="display:inline-block;background:linear-gradient(135deg,#06b6d4,#22d3ee);color:#101114;font-size:16px;font-weight:600;text-decoration:none;padding:14px 40px;border-radius:10px;">
|
||||||
|
Take the Survey
|
||||||
|
</a>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 40px 32px;">
|
||||||
|
<p style="margin:0;color:#5a6170;font-size:12px;text-align:center;">
|
||||||
|
Your responses are confidential. Takes about 5 minutes.
|
||||||
|
</p>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
resend.Emails.send({
|
||||||
|
"from": settings.FROM_EMAIL,
|
||||||
|
"to": [to_email],
|
||||||
|
"subject": subject,
|
||||||
|
"html": email_html,
|
||||||
|
})
|
||||||
|
logger.info("Survey invite email sent to %s", to_email)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to send survey invite email to %s", to_email)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _render_invite_html(
|
def _render_invite_html(
|
||||||
code: str,
|
code: str,
|
||||||
plan_label: str,
|
plan_label: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user