notification_task.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import asyncio
  2. import json
  3. from datetime import datetime
  4. from typing import Dict, Any
  5. from redis.asyncio import Redis
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from app.services.wechat_service import WechatService
  8. from app.services.email_authorizations_service import EmailAuthorizationService
  9. from app.utils.redis_utils import redis_qpop
  10. THROTTLE_EXPIRY = 1800
  11. async def notification_consumer(session_factory, redis_client: Redis):
  12. """
  13. 异步消费 Redis 队列 vas_notification_queue
  14. """
  15. queue_name = "vas_notification_queue"
  16. while True:
  17. try:
  18. # 阻塞获取队列消息
  19. message: Dict[str, Any] = await redis_qpop(redis_client, queue_name, timeout=5)
  20. if not message:
  21. await asyncio.sleep(1) # 队列为空,休眠
  22. continue
  23. channel = message.get("channel", "")
  24. template_id = message.get("template_id")
  25. payload = message.get("payload", {})
  26. # 按渠道发送
  27. if "email" == channel:
  28. content = None
  29. sender = None
  30. subject = None
  31. receiver = message.get("receiver", "")
  32. if "email_verification_for_bind" == template_id:
  33. sender = "donotreply@visafly.top"
  34. subject = "Email Verification"
  35. content = template_for_bind_email(payload)
  36. if "email_verification_for_reset" == template_id:
  37. sender = "donotreply@visafly.top"
  38. subject = "Reset Password"
  39. content = template_for_reset_pwd(payload)
  40. if "login_credentials" == template_id:
  41. sender = "donotreply@visafly.top"
  42. subject = "Your Account Details"
  43. content = template_for_login_credentials(payload)
  44. if "ticket_created" == template_id:
  45. sender = "donotreply@visafly.top"
  46. subject = "Ticket Created"
  47. content = template_ticket_open(payload)
  48. if "appointment_confirmation" == template_id:
  49. sender = "donotreply@visafly.top"
  50. subject = "Appointment Confirmation"
  51. content = template_appointment_confirmation(payload)
  52. if content:
  53. async with session_factory() as db: # type: AsyncSession
  54. auth = await EmailAuthorizationService.get_by_email(db, sender)
  55. send_result = await EmailAuthorizationService.send_email(auth, receiver, subject, "html", content)
  56. print(f"Email send result: {send_result}")
  57. if "wechat" == channel:
  58. api_token = "a8f79817-e18b-4739-8459-adb2ed5e2e32"
  59. if "payment_user_confirmed" == template_id:
  60. status = await WechatService.push_payment_template(api_token, payload)
  61. print(f"Wechat send status: {status}")
  62. if "slot_snapshot" == template_id:
  63. # 1. 提取标识字段
  64. country = payload.get("country", "unknown")
  65. city = payload.get("city", "unknown")
  66. visa_type = payload.get("visa_type", "unknown")
  67. earliest_date = payload.get("earliest_date", "N/A")
  68. # 2. 生成 Redis 频率限制 Key
  69. # 格式: throttle:slot_snapshot:USA:Beijing:B1
  70. throttle_key = f"throttle:slot_snapshot:{country}:{city}:{visa_type}"
  71. # 3. 检查是否存在记录(即是否在冷却期内)
  72. last_sent_val = await redis_client.get(throttle_key)
  73. # 4. 判断是否需要跳过
  74. # 如果记录存在,且 earliest_date 没有变化,则跳过推送
  75. if last_sent_val and last_sent_val == earliest_date:
  76. print(f"⏭️ Skipped redundant Wechat notification for {country}-{city} (In Cooling Period)")
  77. continue
  78. # 5. 执行发送
  79. status = await WechatService.push_slot_snapshot(api_token, payload)
  80. print(f"Wechat send status: {status}")
  81. # 6. 发送成功后更新 Redis 记录并设置过期时间
  82. # 存储当前的最早日期,下次如果日期变了,即便没过 30 分钟也会再次推送
  83. await redis_client.set(throttle_key, str(earliest_date), ex=THROTTLE_EXPIRY)
  84. print(f"✅ Notification sent: {message.get('notification_id')}")
  85. except Exception as e:
  86. print(f"⚠️ Notification consumer error: {e}")
  87. await asyncio.sleep(1) # 避免异常循环过快
  88. def template_for_bind_email(payload):
  89. """
  90. 生成绑定邮箱验证码邮件
  91. Args:
  92. payload (dict): 包含以下字段:
  93. - app_name: 应用名称 (默认 Visafly)
  94. - code: 验证码
  95. - expiration_time: 过期时间描述 (如 "10 minutes")
  96. """
  97. # 1. 定义 HTML 模板
  98. template = '''
  99. <!DOCTYPE html>
  100. <html>
  101. <head>
  102. <meta charset="UTF-8">
  103. <title>Email Verification</title>
  104. <style>
  105. body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
  106. .container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
  107. .code { font-size: 24px; font-weight: bold; letter-spacing: 5px; color: #333; background-color: #f0f0f0; padding: 15px; text-align: center; border-radius: 4px; margin: 20px 0; }
  108. .footer { font-size: 12px; color: #888; margin-top: 30px; text-align: center; }
  109. </style>
  110. </head>
  111. <body>
  112. <div class="container">
  113. <h2>Verify your email address</h2>
  114. <p>Hello,</p>
  115. <p>You requested to bind this email address to your <strong>{{app_name}}</strong> account. Please use the verification code below to proceed:</p>
  116. <div class="code">{{code}}</div>
  117. <p>This code will expire in <strong>{{expiration_time}}</strong>.</p>
  118. <p>If you did not request this change, please ignore this email.</p>
  119. <br>
  120. <p>Best regards,<br>The {{app_name}} Team</p>
  121. <div class="footer">
  122. &copy; 2026 {{app_name}}. All rights reserved.
  123. </div>
  124. </div>
  125. </body>
  126. </html>
  127. '''
  128. # 2. 执行数据替换
  129. # 使用 .get() 提供默认值,防止缺少字段导致报错
  130. # 使用 str() 确保数据是字符串类型
  131. app_name = str(payload.get('app_name', 'Visafly'))
  132. code = str(payload.get('code', ''))
  133. expiration_time = str(payload.get('expiration_time', '10 minutes'))
  134. # 链式替换所有占位符
  135. html_content = template.replace('{{app_name}}', app_name) \
  136. .replace('{{code}}', code) \
  137. .replace('{{expiration_time}}', expiration_time)
  138. return html_content
  139. def template_for_reset_pwd(payload):
  140. """
  141. 生成重置密码验证码邮件
  142. Args:
  143. payload (dict): 包含以下字段:
  144. - app_name: 应用名称 (默认 Visafly)
  145. - code: 验证码
  146. - expiration_time: 过期时间描述 (如 "10 minutes")
  147. """
  148. # 1. 定义 HTML 模板
  149. template = '''
  150. <!DOCTYPE html>
  151. <html>
  152. <head>
  153. <meta charset="UTF-8">
  154. <title>Reset Password</title>
  155. <style>
  156. body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f9f9f9; margin: 0; padding: 0; color: #333; }
  157. .container { max-width: 600px; margin: 40px auto; background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
  158. .header { border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
  159. .header h2 { margin: 0; color: #333; }
  160. .code { font-size: 32px; font-weight: bold; letter-spacing: 5px; color: #2563eb; background-color: #eff6ff; padding: 20px; text-align: center; border-radius: 8px; margin: 30px 0; border: 1px solid #dbeafe; }
  161. .warning { background-color: #fff7ed; border-left: 4px solid #f97316; padding: 15px; font-size: 14px; color: #c2410c; margin-top: 30px; }
  162. .footer { font-size: 12px; color: #999; margin-top: 40px; text-align: center; border-top: 1px solid #eee; padding-top: 20px; }
  163. </style>
  164. </head>
  165. <body>
  166. <div class="container">
  167. <div class="header">
  168. <h2>Password Reset Request</h2>
  169. </div>
  170. <p>Hello,</p>
  171. <p>We received a request to reset the password for your <strong>{{app_name}}</strong> account. Please use the following code to verify your identity:</p>
  172. <div class="code">{{code}}</div>
  173. <p>This code is valid for <strong>{{expiration_time}}</strong>.</p>
  174. <div class="warning">
  175. <strong>Security Tip:</strong> If you did not request a password reset, please ignore this email. No changes will be made to your account.
  176. </div>
  177. <br>
  178. <p>Best regards,<br>The {{app_name}} Team</p>
  179. <div class="footer">
  180. &copy; 2026 {{app_name}}. All rights reserved.<br>
  181. This is an automated message, please do not reply.
  182. </div>
  183. </div>
  184. </body>
  185. </html>
  186. '''
  187. # 2. 执行数据替换
  188. # 使用 .get() 提供默认值,防止缺少字段导致报错
  189. # 使用 str() 确保数据是字符串类型,防止 replace 报错
  190. app_name = str(payload.get('app_name', 'Visafly'))
  191. code = str(payload.get('code', ''))
  192. expiration_time = str(payload.get('expiration_time', '10 minutes'))
  193. html_content = template.replace('{{app_name}}', app_name) \
  194. .replace('{{code}}', code) \
  195. .replace('{{expiration_time}}', expiration_time)
  196. return html_content
  197. def template_for_login_credentials(payload):
  198. """
  199. 生成用户登录凭证邮件 (账号 + 临时密码)
  200. Args:
  201. payload (dict): 包含以下字段:
  202. - app_name: 应用名称
  203. - username: 登录账号
  204. - password: 临时密码
  205. - login_url: 登录链接
  206. """
  207. # 1. 定义 HTML 模板
  208. template = '''
  209. <!DOCTYPE html>
  210. <html>
  211. <head>
  212. <meta charset="UTF-8">
  213. <title>Your Account Details</title>
  214. <style>
  215. body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
  216. .container { max-width: 600px; margin: 40px auto; background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
  217. .header { text-align: center; border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
  218. .header h1 { font-size: 24px; color: #1a1a1a; margin: 0; }
  219. .creds-box { background-color: #f0f7ff; border: 1px solid #dbeafe; border-radius: 8px; padding: 20px; margin: 25px 0; }
  220. .creds-item { margin-bottom: 10px; font-size: 16px; }
  221. .creds-label { font-weight: bold; color: #555; width: 100px; display: inline-block; }
  222. .creds-value { font-family: 'Courier New', Courier, monospace; font-weight: bold; color: #2563eb; }
  223. .btn { display: block; width: 200px; margin: 30px auto; background-color: #2563eb; color: #ffffff !important; text-align: center; padding: 12px 0; border-radius: 6px; text-decoration: none; font-weight: bold; }
  224. .note { font-size: 13px; color: #666; background-color: #fff4e5; padding: 10px; border-radius: 4px; border-left: 4px solid #f97316; }
  225. .footer { font-size: 12px; color: #999; margin-top: 40px; text-align: center; }
  226. </style>
  227. </head>
  228. <body>
  229. <div class="container">
  230. <div class="header">
  231. <h1>Welcome to {{app_name}}</h1>
  232. </div>
  233. <p>Dear User,</p>
  234. <p>Your account has been successfully set up. Below are your temporary login credentials.</p>
  235. <div class="creds-box">
  236. <div class="creds-item">
  237. <span class="creds-label">Username:</span>
  238. <span class="creds-value">{{username}}</span>
  239. </div>
  240. <div class="creds-item">
  241. <span class="creds-label">Password:</span>
  242. <span class="creds-value">{{password}}</span>
  243. </div>
  244. </div>
  245. <div class="note">
  246. <strong>Important:</strong> For your security, please change your password immediately after logging in.
  247. </div>
  248. <a href="{{login_url}}" class="btn">Log In Now</a>
  249. <p style="text-align: center; font-size: 14px;">
  250. Or copy this link: <a href="{{login_url}}">{{login_url}}</a>
  251. </p>
  252. <div class="footer">
  253. &copy; 2026 {{app_name}}. All rights reserved.<br>
  254. If you did not request this account, please contact support.
  255. </div>
  256. </div>
  257. </body>
  258. </html>
  259. '''
  260. # 2. 执行数据替换
  261. # 使用 payload.get() 提供默认值,防止缺少字段导致报错
  262. app_name = str(payload.get('app_name', 'Visafly'))
  263. username = str(payload.get('username', ''))
  264. password = str(payload.get('password', ''))
  265. login_url = str(payload.get('login_url', '#'))
  266. # 链式替换所有占位符
  267. html_content = template.replace('{{app_name}}', app_name) \
  268. .replace('{{username}}', username) \
  269. .replace('{{password}}', password) \
  270. .replace('{{login_url}}', login_url)
  271. return html_content
  272. def template_ticket_open(payload):
  273. """
  274. 生成工单创建通知邮件
  275. payload 需包含: username, ticket_id, ticket_type, created_at, ticket_url, app_name
  276. """
  277. # --- 1. 处理时间格式化逻辑 ---
  278. raw_time = payload.get('created_at')
  279. formatted_time = ""
  280. if isinstance(raw_time, datetime):
  281. # 如果传入的是 datetime 对象
  282. formatted_time = raw_time.strftime('%Y-%m-%d %H:%M') + " (UTC)"
  283. elif isinstance(raw_time, str):
  284. try:
  285. # 如果传入的是 ISO 字符串 (例如 '2025-12-31T02:33:00Z')
  286. # 截取前19位通常能兼容大部分 ISO 格式
  287. dt_obj = datetime.fromisoformat(raw_time.replace('Z', '+00:00'))
  288. formatted_time = dt_obj.strftime('%Y-%m-%d %H:%M') + " (UTC)"
  289. except ValueError:
  290. # 如果解析失败,直接显示原字符串
  291. formatted_time = raw_time
  292. else:
  293. formatted_time = "N/A"
  294. # --- 2. HTML 模板 ---
  295. # 注意:这里保持了 {{key}} 占位符,下面会统一替换
  296. template = '''
  297. <!DOCTYPE html>
  298. <html>
  299. <head>
  300. <meta charset="UTF-8">
  301. <title>Ticket Created</title>
  302. <style>
  303. body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
  304. .container { max-width: 600px; margin: 40px auto; background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
  305. .header { border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
  306. .header h1 { font-size: 22px; color: #1a1a1a; margin: 0; }
  307. .ticket-info { background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin: 20px 0; }
  308. .info-row { margin-bottom: 10px; display: flex; justify-content: space-between; }
  309. .info-label { color: #64748b; font-size: 14px; }
  310. .info-value { font-weight: bold; color: #0f172a; font-size: 14px; }
  311. .btn { display: block; width: 200px; margin: 30px auto; background-color: #2563eb; color: #ffffff !important; text-align: center; padding: 12px 0; border-radius: 6px; text-decoration: none; font-weight: bold; font-size: 14px; }
  312. .footer { font-size: 12px; color: #94a3b8; margin-top: 40px; text-align: center; }
  313. </style>
  314. </head>
  315. <body>
  316. <div class="container">
  317. <div class="header">
  318. <h1>Support Request Received</h1>
  319. </div>
  320. <p>Hello {{username}},</p>
  321. <p>We wanted to let you know that we've received your request. Our team is currently reviewing the details.</p>
  322. <div class="ticket-info">
  323. <div class="info-row">
  324. <span class="info-label">Ticket ID:</span>
  325. <span class="info-value">#{{ticket_id}}</span>
  326. </div>
  327. <div class="info-row">
  328. <span class="info-label">Type:</span>
  329. <span class="info-value">{{ticket_type}}</span>
  330. </div>
  331. <div class="info-row" style="margin-bottom: 0;">
  332. <span class="info-label">Created Time:</span>
  333. <!-- 使用格式化后的时间 -->
  334. <span class="info-value">{{created_at}}</span>
  335. </div>
  336. </div>
  337. <p>We usually reply within 24 hours. You will receive an email notification when our agent replies.</p>
  338. <a href="{{ticket_url}}" class="btn">View Ticket Details</a>
  339. <div class="footer">
  340. &copy; 2025 {{app_name}}. All rights reserved.<br>
  341. Please do not reply to this email directly.
  342. </div>
  343. </div>
  344. </body>
  345. </html>
  346. '''
  347. # --- 3. 执行替换 ---
  348. # 使用 payload 中的数据替换模板占位符
  349. html_content = template.replace('{{username}}', str(payload.get('username', 'User'))) \
  350. .replace('{{ticket_id}}', str(payload.get('ticket_id', ''))) \
  351. .replace('{{ticket_type}}', str(payload.get('ticket_type', ''))) \
  352. .replace('{{created_at}}', formatted_time) \
  353. .replace('{{ticket_url}}', str(payload.get('ticket_url', '#'))) \
  354. .replace('{{app_name}}', str(payload.get('app_name', 'Visafly')))
  355. return html_content
  356. def template_appointment_confirmation(payload):
  357. """
  358. 生成预约成功确认邮件 (VisaFly)
  359. payload 需包含:
  360. - username: 用户名
  361. - order_id: 订单号 (新增)
  362. - country: 国家
  363. - city: 城市
  364. - appointment_date: 预约时间 (字符串, 例如 "2026-03-15 09:00")
  365. - visa_type: 签证类型
  366. - user_email: 用户邮箱 (用于提示信件已发往此处)
  367. """
  368. # --- 1. 基础配置 (VisaFly) ---
  369. company_name = "VisaFly"
  370. support_email = "support@visafly.top"
  371. website_home = "https://visafly.top"
  372. website_contact = "https://visafly.top/refund-policy"
  373. # --- 2. HTML 模板 ---
  374. template = '''
  375. <!DOCTYPE html>
  376. <html>
  377. <head>
  378. <meta charset="UTF-8">
  379. <title>Appointment Confirmed</title>
  380. <style>
  381. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
  382. .container { max-width: 600px; margin: 30px auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
  383. /* 头部: 品牌蓝 */
  384. .header { background-color: #0056b3; padding: 30px 20px; text-align: center; color: white; }
  385. .header h1 { margin: 0; font-size: 24px; font-weight: bold; }
  386. .header .subtitle { margin-top: 5px; opacity: 0.9; font-size: 14px; }
  387. /* 正文区域 */
  388. .content { padding: 30px 25px; line-height: 1.6; }
  389. /* 核心警告框 (黄色高亮 - 强调查收邮件) */
  390. .alert-box { background-color: #fff8e1; border-left: 5px solid #ffc107; padding: 15px 20px; margin: 25px 0; border-radius: 4px; }
  391. .alert-title { font-weight: bold; color: #b00020; display: block; margin-bottom: 5px; font-size: 16px; }
  392. /* 订单详情卡片 */
  393. .details-box { background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 15px; margin-bottom: 25px; }
  394. .info-row { display: flex; justify-content: space-between; margin-bottom: 10px; border-bottom: 1px dashed #e2e8f0; padding-bottom: 8px; }
  395. .info-row:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
  396. .label { color: #64748b; font-size: 13px; font-weight: 500; }
  397. .value { font-weight: bold; color: #0f172a; font-size: 14px; text-align: right; }
  398. /* 底部联系方式 */
  399. .help-section { background-color: #f1f5f9; padding: 20px; text-align: center; font-size: 14px; color: #475569; border-top: 1px solid #e2e8f0; }
  400. .contact-link { color: #0056b3; font-weight: bold; text-decoration: none; margin: 0 5px; }
  401. .footer { text-align: center; padding: 20px; font-size: 12px; color: #94a3b8; }
  402. a { color: #0056b3; text-decoration: none; }
  403. </style>
  404. </head>
  405. <body>
  406. <div class="container">
  407. <div class="header">
  408. <h1>✅ Booking Successful!</h1>
  409. <div class="subtitle">Appointment Secured by {{company_name}}</div>
  410. </div>
  411. <div class="content">
  412. <p>Hello <b>{{username}}</b>,</p>
  413. <p>Great news! We have successfully secured your appointment slot.</p>
  414. <!-- 详情卡片 -->
  415. <div class="details-box">
  416. <div class="info-row">
  417. <span class="label">Order ID:</span>
  418. <span class="value">#{{order_id}}</span>
  419. </div>
  420. <div class="info-row">
  421. <span class="label">Country / City:</span>
  422. <span class="value">{{country}} - {{city}}</span>
  423. </div>
  424. <div class="info-row">
  425. <span class="label">Appointment Date:</span>
  426. <span class="value">{{appointment_date}}</span>
  427. </div>
  428. <div class="info-row">
  429. <span class="label">Visa Type:</span>
  430. <span class="value">{{visa_type}}</span>
  431. </div>
  432. </div>
  433. <!-- 核心:检查邮件提示 -->
  434. <div class="alert-box">
  435. <span class="alert-title">📩 Important: Check Your Email</span>
  436. We have sent the official confirmation letter to <b>{{user_email}}</b>.<br><br>
  437. <span style="font-size: 13px;">If you don't see it in your Inbox, please check your <b>Spam/Junk</b> folder immediately.</span>
  438. </div>
  439. </div>
  440. <!-- 兜底联系方式 -->
  441. <div class="help-section">
  442. <p style="margin-top: 0; font-weight: bold;">Did not receive the email?</p>
  443. <p style="margin-bottom: 15px;">Please check your Spam folder first. If still missing, contact us:</p>
  444. <!-- 方式1: 邮件 -->
  445. <a href="mailto:{{support_email}}" class="contact-link">✉️ Email Support</a>
  446. <span style="color: #cbd5e1;">|</span>
  447. <!-- 方式2: 官网 -->
  448. <a href="{{website_contact}}" class="contact-link">🌐 Contact Us</a>
  449. </div>
  450. <div class="footer">
  451. &copy; 2026 {{company_name}}. All rights reserved.<br>
  452. <a href="{{website_home}}">{{website_home}}</a>
  453. </div>
  454. </div>
  455. </body>
  456. </html>
  457. '''
  458. # --- 3. 执行替换 ---
  459. html_content = template.replace('{{username}}', str(payload.get('username', 'Customer'))) \
  460. .replace('{{order_id}}', str(payload.get('order_id', 'N/A'))) \
  461. .replace('{{country}}', str(payload.get('country', ''))) \
  462. .replace('{{city}}', str(payload.get('city', ''))) \
  463. .replace('{{appointment_date}}', str(payload.get('appointment_date', 'Confirmed'))) \
  464. .replace('{{user_email}}', str(payload.get('user_email', 'your email'))) \
  465. .replace('{{visa_type}}', str(payload.get('visa_type', 'Standard'))) \
  466. .replace('{{company_name}}', company_name) \
  467. .replace('{{support_email}}', support_email) \
  468. .replace('{{website_contact}}', website_contact) \
  469. .replace('{{website_home}}', website_home)
  470. return html_content