notification_templates.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. from typing import Dict, Tuple, Any, List
  2. # ==========================================
  3. # 0. Internal Helpers
  4. # ==========================================
  5. def _format_slot_summary(availability: Any) -> str:
  6. if not isinstance(availability, list) or not availability:
  7. return "No specific time slots recorded."
  8. items = []
  9. for day in availability[:4]:
  10. date = day.get("date", "")
  11. # Minimalist text for anti-spam
  12. items.append(f"- {date}")
  13. return "\n".join(items)
  14. def _format_currency(amount_in_cents: Any) -> str:
  15. """将分转换为元,保留两位小数"""
  16. try:
  17. # 确保是数字类型
  18. value = float(amount_in_cents) / 100.0
  19. return f"{value:.2f}"
  20. except (ValueError, TypeError):
  21. return str(amount_in_cents)
  22. def _get_status_meta(status: str) -> Tuple[str, str, str]:
  23. if status == 'Available':
  24. return "READY", "#16a34a", "Available"
  25. if status == 'Waitlist':
  26. return "WAIT", "#ca8a04", "Waitlist Only"
  27. return "NONE", "#dc2626", "No Slots"
  28. # ==========================================
  29. # 1. EMAIL TEMPLATES (Anti-Spam Optimized)
  30. # ==========================================
  31. def _email_base(content_html: str, app_name: str = "Visafly") -> str:
  32. """
  33. Ultra-clean HTML structure.
  34. Uses standard fonts and minimal CSS to pass through ESP filters.
  35. """
  36. return f"""
  37. <!DOCTYPE html>
  38. <html lang="en">
  39. <head>
  40. <meta charset="UTF-8">
  41. <title>Notification</title>
  42. <style>
  43. /* Standard fonts only */
  44. body {{ font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-size: 16px; line-height: 1.5; color: #333333; margin: 0; padding: 0; }}
  45. .wrapper {{ background-color: #f6f9fc; padding: 20px; }}
  46. .container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e1e4e8; border-radius: 4px; }}
  47. .content {{ padding: 30px; }}
  48. .footer {{ padding: 20px; text-align: center; font-size: 12px; color: #777777; }}
  49. .button {{ display: inline-block; padding: 10px 20px; background-color: #0052cc; color: #ffffff !important; text-decoration: none; border-radius: 3px; }}
  50. .code {{ font-family: monospace; font-size: 24px; font-weight: bold; background: #f4f4f4; padding: 10px; border-radius: 4px; display: inline-block; letter-spacing: 2px; }}
  51. hr {{ border: 0; border-top: 1px solid #eeeeee; margin: 20px 0; }}
  52. </style>
  53. </head>
  54. <body>
  55. <div class="wrapper">
  56. <div class="container">
  57. <div class="content">
  58. {content_html}
  59. <hr>
  60. <p style="font-size: 13px; color: #888;">
  61. Regards,<br>
  62. <strong>{app_name} Team</strong>
  63. </p>
  64. </div>
  65. </div>
  66. <div class="footer">
  67. You received this because you are a registered user of {app_name}.<br>
  68. To manage your notification settings, please visit our website.<br>
  69. © 2026 {app_name}. Support: contact@visafly.top
  70. </div>
  71. </div>
  72. </body>
  73. </html>
  74. """
  75. def template_email_verification_bind(payload: Dict) -> str:
  76. # Use neutral language: "Verification code" instead of "URGENT ACTION"
  77. html = f"""
  78. <h2 style="font-size: 20px; color: #111;">Email Verification</h2>
  79. <p>You requested to link this email address to your {payload.get('app_name', 'Visafly')} account.</p>
  80. <p>Your verification code is:</p>
  81. <div class="code">{payload.get('code')}</div>
  82. <p>This code will expire in {payload.get('expiration_time', '10 minutes')}.</p>
  83. <p>If you did not make this request, you can safely ignore this email.</p>
  84. """
  85. return _email_base(html, payload.get('app_name', 'Visafly'))
  86. def template_email_password_reset(payload: Dict) -> str:
  87. html = f"""
  88. <h2 style="font-size: 20px; color: #111;">Password Reset Code</h2>
  89. <p>We received a request to reset your password. Please enter the following code to continue:</p>
  90. <div class="code">{payload.get('code')}</div>
  91. <p>Valid for {payload.get('expiration_time', '10 minutes')}. For security reasons, do not share this code.</p>
  92. """
  93. return _email_base(html, payload.get('app_name', 'Visafly'))
  94. def template_email_login_credentials(payload: Dict) -> str:
  95. html = f"""
  96. <h2 style="font-size: 20px; color: #111;">Your Account Credentials</h2>
  97. <p>Your account has been successfully created. Here are your temporary login details:</p>
  98. <table style="width: 100%; background: #f9f9f9; padding: 15px; border-radius: 4px;">
  99. <tr><td><strong>User:</strong></td><td>{payload.get('username')}</td></tr>
  100. <tr><td><strong>Pass:</strong></td><td><code>{payload.get('password')}</code></td></tr>
  101. </table>
  102. <p><a href="{payload.get('login_url')}" class="button">Log in to your account</a></p>
  103. """
  104. return _email_base(html, payload.get('app_name', 'Visafly'))
  105. def template_email_slot_subscription(payload: Dict) -> str:
  106. # Change "Alert" to "Update" to sound less like spam
  107. html = f"""
  108. <h2 style="font-size: 20px; color: #111;">Visa Slot Update</h2>
  109. <p>This is an automated update regarding your visa slot subscription.</p>
  110. <div style="border-left: 3px solid #0052cc; padding-left: 15px; margin: 20px 0;">
  111. <strong>Location:</strong> {payload.get('country')}, {payload.get('city')}<br>
  112. <strong>Visa Type:</strong> {payload.get('visa_type')}<br>
  113. <strong>Earliest Date:</strong> <span style="color:#d93025;">{payload.get('earliest_date')}</span>
  114. </div>
  115. <p><a href="{payload.get('website')}" class="button">Check Official Website</a></p>
  116. """
  117. return _email_base(html)
  118. def template_email_appointment_confirmation(payload: Dict) -> str:
  119. """预约成功确认邮件 (Anti-Spam Optimized)"""
  120. html = f"""
  121. <h2 style="font-size: 20px; color: #111;">Appointment Confirmed</h2>
  122. <p>Dear {payload.get('username', 'Customer')},</p>
  123. <p>We are pleased to inform you that your visa appointment has been successfully booked.</p>
  124. <table style="width: 100%; background: #f9f9f9; padding: 15px; border-radius: 4px; border-collapse: separate; border-spacing: 0 10px; text-align: left;">
  125. <tr>
  126. <td style="width: 35%; color: #555;"><strong>Order ID:</strong></td>
  127. <td><code>{payload.get('order_id', 'N/A')}</code></td>
  128. </tr>
  129. <tr>
  130. <td style="color: #555;"><strong>Location:</strong></td>
  131. <td>{payload.get('country', '')}, {payload.get('city', '')}</td>
  132. </tr>
  133. <tr>
  134. <td style="color: #555;"><strong>Visa Type:</strong></td>
  135. <td>{payload.get('visa_type', 'N/A')}</td>
  136. </tr>
  137. <tr>
  138. <td style="color: #555;"><strong>Appointment Date:</strong></td>
  139. <td><strong style="color: #0052cc;">{payload.get('appointment_date', 'N/A')}</strong></td>
  140. </tr>
  141. <tr>
  142. <td style="color: #555;"><strong>Account Email:</strong></td>
  143. <td>{payload.get('user_email', 'N/A')}</td>
  144. </tr>
  145. </table>
  146. <p>Please ensure you prepare all required documents well in advance of your appointment date. If you have any questions, feel free to contact our support team.</p>
  147. """
  148. return _email_base(html, payload.get('app_name', 'Visafly'))
  149. def template_email_order_event_update(payload: Dict) -> str:
  150. """
  151. Template for visa progress updates.
  152. Focuses on neutral language to ensure inbox delivery.
  153. """
  154. order_no = payload.get('order_no', 'N/A')
  155. summary = payload.get('summary', 'No summary available.')
  156. # Neutral subject line used in get_email_meta: "Notification: Visa Appointment Update"
  157. html = f"""
  158. <h2 style="font-size: 20px; color: #111;">Visa Appointment Update</h2>
  159. <p>We have received a new update regarding your visa process for <strong>Order #{order_no}</strong>.</p>
  160. <div style="border-left: 3px solid #0052cc; padding: 15px; margin: 20px 0; background-color: #f9fafb; border-radius: 0 4px 4px 0;">
  161. <strong style="color: #555; font-size: 13px; text-transform: uppercase;">Update Summary:</strong><br>
  162. <p style="margin-top: 5px; color: #333;">{summary}</p>
  163. </div>
  164. <p>For your security and privacy, please log in to your dashboard to view the full message and any relevant documents.</p>
  165. <p style="margin-top: 30px;">
  166. <a href="https://visafly.top/dashboard" class="button">Access Dashboard</a>
  167. </p>
  168. """
  169. return _email_base(html, "Visafly")
  170. # ==========================================
  171. # 2. WECHAT / TELEGRAM / WHATSAPP (Keeping the styles)
  172. # ==========================================
  173. # [Note: These channels don't have spam filters based on HTML structure,
  174. # so we keep the rich formatting we designed previously.]
  175. def template_wechat_payment_confirmed(payload: Dict) -> str:
  176. """支付成功通知模板"""
  177. amount_decimal = _format_currency(payload.get('amount'))
  178. return (
  179. f"# ✅ Payment Success\n"
  180. f"--- \n"
  181. f"💰 **Amount:** {amount_decimal} {payload.get('currency')}\n"
  182. f"🆔 **Order ID:** `{payload.get('order_id')}`\n"
  183. f"📧 **User Account:** {payload.get('user_email')}\n"
  184. f"🕒 **Time:** {payload.get('confirmed_at')}\n\n"
  185. f"Your service has been activated. Please stay tuned for further updates."
  186. )
  187. def template_wechat_appointment_confirmation(payload: Dict) -> str:
  188. """预约成功微信通知 (Markdown)"""
  189. return (
  190. f"# 🎉 Appointment Confirmed\n"
  191. f"--- \n"
  192. f"👤 **User:** {payload.get('username', 'Customer')}\n"
  193. f"📍 **Location:** {payload.get('country')}, {payload.get('city')}\n"
  194. f"🛂 **Visa Type:** {payload.get('visa_type')}\n"
  195. f"📅 **Date:** `{payload.get('appointment_date')}`\n"
  196. f"🆔 **Order ID:** `{payload.get('order_id')}`\n\n"
  197. f"Your appointment is successfully booked. Please prepare your documents."
  198. )
  199. def template_wechat_slot_snapshot(payload: Dict) -> str:
  200. emoji, _, status_text = _get_status_meta(payload.get("availability_status"))
  201. summary = _format_slot_summary(payload.get("availability"))
  202. return (
  203. f"# {emoji} {payload.get('country')} {payload.get('city')}\n"
  204. f"> {status_text} | {payload.get('visa_type')}\n\n"
  205. f"📅 **Earliest Date:** `{payload.get('earliest_date', 'N/A')}`\n\n"
  206. f"🔍 **Slots Overview:**\n{summary}\n\n"
  207. f"🔗 [Book Now]({payload.get('website', '#')})"
  208. )
  209. def template_telegram_slot_snapshot(payload: Dict) -> str:
  210. emoji, _, status_text = _get_status_meta(payload.get("availability_status"))
  211. summary = _format_slot_summary(payload.get("availability"))
  212. return (
  213. f"{emoji} <b>{payload.get('country')}, {payload.get('city')}</b>\n"
  214. f"<i>{status_text} · {payload.get('visa_type')}</i>\n"
  215. f"──────────────────\n"
  216. f"📅 <b>Earliest Date:</b> <code>{payload.get('earliest_date', 'N/A')}</code>\n\n"
  217. f"📊 <b>Availability:</b>\n{summary}\n"
  218. f"──────────────────\n"
  219. f"🔗 <a href='{payload.get('website', '#')}'><b>Official Website ➜</b></a>\n\n"
  220. )
  221. def template_telegram_appointment_confirmation(payload: Dict) -> str:
  222. """预约成功 TG 通知 (HTML)"""
  223. return (
  224. f"🎉 <b>Appointment Confirmed</b>\n"
  225. f"──────────────────\n"
  226. f"👤 <b>Name:</b> {payload.get('username', 'Customer')}\n"
  227. f"📍 <b>Location:</b> {payload.get('country')}, {payload.get('city')}\n"
  228. f"🛂 <b>Visa Type:</b> {payload.get('visa_type')}\n"
  229. f"📅 <b>Date:</b> <code>{payload.get('appointment_date')}</code>\n"
  230. f"──────────────────\n"
  231. f"🆔 <i>Order: {payload.get('order_id')}</i>\n"
  232. f"📧 <i>Email: {payload.get('user_email')}</i>"
  233. )
  234. def template_telegram_order_event_update(payload: Dict) -> str:
  235. summary = payload.get('summary', 'New status update received.')
  236. return (
  237. f"🔔 <b>Visa Progress Update</b>\n"
  238. f"──────────────────\n"
  239. f"🆔 <b>Order:</b> #{payload.get('order_no')}\n"
  240. f"📊 <b>Summary:</b>\n<i>{summary}</i>\n"
  241. f"──────────────────\n"
  242. f"🔗 <a href='https://visafly.top/dashboard'><b>View Dashboard ➜</b></a>"
  243. )
  244. def template_whatsapp_slot_snapshot(payload: Dict) -> str:
  245. emoji, _, _ = _get_status_meta(payload.get("availability_status"))
  246. return (
  247. f"{emoji} *VISA SLOT UPDATE*\n\n"
  248. f"📍 *Location:* {payload.get('country')}, {payload.get('city')}\n"
  249. f"🛂 *Type:* {payload.get('visa_type')}\n"
  250. f"📅 *Earliest:* {payload.get('earliest_date')}\n\n"
  251. f"🌐 *Book Now:* {payload.get('website')}"
  252. )
  253. def template_whatsapp_appointment_confirmation(payload: Dict) -> str:
  254. """预约成功 WhatsApp 通知 (Clean Text)"""
  255. return (
  256. f"✅ *APPOINTMENT CONFIRMED*\n\n"
  257. f"Dear {payload.get('username', 'Customer')},\n"
  258. f"Your visa appointment has been successfully booked.\n\n"
  259. f"📍 *Location:* {payload.get('country')}, {payload.get('city')}\n"
  260. f"🛂 *Visa Type:* {payload.get('visa_type')}\n"
  261. f"📅 *Date:* {payload.get('appointment_date')}\n"
  262. f"🆔 *Order ID:* {payload.get('order_id')}\n\n"
  263. f"Please ensure all required documents are prepared. Thank you for choosing Visafly."
  264. )
  265. def template_whatsapp_order_event_update(payload: Dict) -> str:
  266. """
  267. WhatsApp template for progress updates.
  268. """
  269. order_no = payload.get('order_no', 'N/A')
  270. # If the service pre-generated a message, use it, otherwise build one
  271. message = payload.get('message')
  272. if not message:
  273. summary = payload.get('summary', 'New status update received.')
  274. message = (
  275. f"🔔 *VISA PROGRESS UPDATE*\n\n"
  276. f"Order: #{order_no}\n\n"
  277. f"Latest Update:\n"
  278. f"_{summary}_\n\n"
  279. f"Please log in to your dashboard or check your email for full details."
  280. )
  281. return message
  282. # ==========================================
  283. # Dispatchers (保持不变)
  284. # ==========================================
  285. EMAIL_MAP = {
  286. "email_verification_for_bind": (template_email_verification_bind, "Verification code for your account"),
  287. "email_verification_for_reset": (template_email_password_reset, "Password reset code"),
  288. "login_credentials": (template_email_login_credentials, "Your login credentials"),
  289. "slot_subscription": (template_email_slot_subscription, "Update: Visa availability change"),
  290. "appointment_confirmation": (template_email_appointment_confirmation, "Your Visa Appointment Confirmation"),
  291. "order_event_update": (template_email_order_event_update, "Notification: Visa Appointment Update"),
  292. }
  293. WECHAT_MAP = {
  294. "slot_snapshot": template_wechat_slot_snapshot,
  295. "payment_user_confirmed": template_wechat_payment_confirmed,
  296. "appointment_confirmation": template_wechat_appointment_confirmation,
  297. }
  298. WHATSAPP_MAP = {
  299. "slot_subscription": template_whatsapp_slot_snapshot,
  300. "slot_snapshot": template_whatsapp_slot_snapshot,
  301. "appointment_confirmation": template_whatsapp_appointment_confirmation,
  302. "order_event_update": template_whatsapp_order_event_update,
  303. }
  304. TELEGRAM_MAP = {
  305. "slot_snapshot": template_telegram_slot_snapshot,
  306. "appointment_confirmation": template_telegram_appointment_confirmation,
  307. "order_event_update": template_telegram_order_event_update,
  308. }
  309. def render_email(tid: str, payload: Dict) -> str:
  310. return EMAIL_MAP[tid][0](payload) if tid in EMAIL_MAP else "Notification from Visafly"
  311. def get_email_meta(tid: str) -> Tuple[str, str]:
  312. sender = "donotreply@visafly.top"
  313. return (sender, EMAIL_MAP[tid][1]) if tid in EMAIL_MAP else (sender, "Notification")
  314. def render_wechat_markdown(tid: str, payload: Dict) -> str:
  315. return WECHAT_MAP[tid](payload) if tid in WECHAT_MAP else str(payload)
  316. def render_whatsapp_text(tid: str, payload: Dict) -> str:
  317. return WHATSAPP_MAP[tid](payload) if tid in WHATSAPP_MAP else str(payload)
  318. def render_telegram_html(tid: str, payload: Dict) -> str:
  319. return TELEGRAM_MAP[tid](payload) if tid in TELEGRAM_MAP else str(payload)