9a2aea038a
- Add pyproject.toml with uv and ruff configuration - Pin Python version to 3.13 via .python-version - Add Makefile commands: lint, format, fix - Apply ruff formatting to entire codebase - Remove unused imports (base64 in yookassa/simple_subscription) - Update .gitignore for new config files
912 lines
38 KiB
Python
912 lines
38 KiB
Python
"""
|
||
Email notification templates for different notification types.
|
||
|
||
Supports multiple languages: ru, en, zh, ua
|
||
"""
|
||
|
||
from typing import Any
|
||
|
||
from app.config import settings
|
||
|
||
|
||
class EmailNotificationTemplates:
|
||
"""HTML email templates for user notifications."""
|
||
|
||
def __init__(self):
|
||
self.service_name = settings.SMTP_FROM_NAME or 'VPN Service'
|
||
self.cabinet_url = getattr(settings, 'CABINET_URL', '')
|
||
|
||
def get_template(
|
||
self,
|
||
notification_type: 'NotificationType',
|
||
language: str,
|
||
context: dict[str, Any],
|
||
) -> dict[str, str] | None:
|
||
"""
|
||
Get email template for notification type.
|
||
|
||
Args:
|
||
notification_type: Type of notification
|
||
language: Language code (ru, en, zh, ua)
|
||
context: Context data for template rendering
|
||
|
||
Returns:
|
||
Dict with 'subject', 'body_html', and optionally 'body_text'
|
||
"""
|
||
# Import here to avoid circular imports
|
||
from app.services.notification_delivery_service import NotificationType
|
||
|
||
template_map = {
|
||
NotificationType.BALANCE_TOPUP: self._balance_topup_template,
|
||
NotificationType.BALANCE_CHANGE: self._balance_change_template,
|
||
NotificationType.SUBSCRIPTION_EXPIRING: self._subscription_expiring_template,
|
||
NotificationType.SUBSCRIPTION_EXPIRED: self._subscription_expired_template,
|
||
NotificationType.SUBSCRIPTION_RENEWED: self._subscription_renewed_template,
|
||
NotificationType.SUBSCRIPTION_ACTIVATED: self._subscription_activated_template,
|
||
NotificationType.AUTOPAY_SUCCESS: self._autopay_success_template,
|
||
NotificationType.AUTOPAY_FAILED: self._autopay_failed_template,
|
||
NotificationType.AUTOPAY_INSUFFICIENT_FUNDS: self._autopay_insufficient_funds_template,
|
||
NotificationType.DAILY_DEBIT: self._daily_debit_template,
|
||
NotificationType.DAILY_INSUFFICIENT_FUNDS: self._daily_insufficient_funds_template,
|
||
NotificationType.BAN_NOTIFICATION: self._ban_template,
|
||
NotificationType.UNBAN_NOTIFICATION: self._unban_template,
|
||
NotificationType.WARNING_NOTIFICATION: self._warning_template,
|
||
NotificationType.REFERRAL_BONUS: self._referral_bonus_template,
|
||
NotificationType.REFERRAL_REGISTERED: self._referral_registered_template,
|
||
NotificationType.TRAFFIC_RESET: self._traffic_reset_template,
|
||
NotificationType.PAYMENT_RECEIVED: self._payment_received_template,
|
||
}
|
||
|
||
template_func = template_map.get(notification_type)
|
||
if not template_func:
|
||
return None
|
||
|
||
return template_func(language, context)
|
||
|
||
def _get_base_template(self, content: str) -> str:
|
||
"""Wrap content in base HTML template."""
|
||
return f"""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<style>
|
||
body {{
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
background-color: #f5f5f5;
|
||
margin: 0;
|
||
padding: 0;
|
||
}}
|
||
.container {{
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
background-color: #ffffff;
|
||
}}
|
||
.header {{
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
border-bottom: 2px solid #007bff;
|
||
}}
|
||
.header h1 {{
|
||
color: #007bff;
|
||
margin: 0;
|
||
font-size: 24px;
|
||
}}
|
||
.content {{
|
||
padding: 30px 20px;
|
||
}}
|
||
.highlight {{
|
||
background-color: #f8f9fa;
|
||
border-left: 4px solid #007bff;
|
||
padding: 15px;
|
||
margin: 20px 0;
|
||
}}
|
||
.success {{
|
||
border-left-color: #28a745;
|
||
}}
|
||
.warning {{
|
||
border-left-color: #ffc107;
|
||
}}
|
||
.danger {{
|
||
border-left-color: #dc3545;
|
||
}}
|
||
.button {{
|
||
display: inline-block;
|
||
padding: 12px 24px;
|
||
background-color: #007bff;
|
||
color: white !important;
|
||
text-decoration: none;
|
||
border-radius: 5px;
|
||
margin: 20px 0;
|
||
font-weight: bold;
|
||
}}
|
||
.button:hover {{
|
||
background-color: #0056b3;
|
||
}}
|
||
.footer {{
|
||
margin-top: 30px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #eee;
|
||
font-size: 12px;
|
||
color: #666;
|
||
text-align: center;
|
||
}}
|
||
.amount {{
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #28a745;
|
||
}}
|
||
.amount.negative {{
|
||
color: #dc3545;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>{self.service_name}</h1>
|
||
</div>
|
||
<div class="content">
|
||
{content}
|
||
</div>
|
||
<div class="footer">
|
||
<p>© {self.service_name}. All rights reserved.</p>
|
||
<p>This is an automated message. Please do not reply to this email.</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
def _get_cabinet_button(self, language: str) -> str:
|
||
"""Get cabinet link button HTML."""
|
||
if not self.cabinet_url:
|
||
return ''
|
||
|
||
texts = {
|
||
'ru': 'Открыть личный кабинет',
|
||
'en': 'Open Dashboard',
|
||
'zh': '打开控制面板',
|
||
'ua': 'Відкрити особистий кабінет',
|
||
}
|
||
text = texts.get(language, texts['en'])
|
||
|
||
return f'<p style="text-align: center;"><a href="{self.cabinet_url}" class="button">{text}</a></p>'
|
||
|
||
# ============================================================================
|
||
# Balance Templates
|
||
# ============================================================================
|
||
|
||
def _balance_topup_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for balance top-up notification."""
|
||
amount = context.get('formatted_amount', f'{context.get("amount_rubles", 0):.2f} ₽')
|
||
balance = context.get('formatted_balance', f'{context.get("new_balance_rubles", 0):.2f} ₽')
|
||
|
||
subjects = {
|
||
'ru': f'Баланс пополнен на {amount}',
|
||
'en': f'Balance topped up by {amount}',
|
||
'zh': f'余额已充值 {amount}',
|
||
'ua': f'Баланс поповнено на {amount}',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Баланс успешно пополнен!</h2>
|
||
<div class="highlight success">
|
||
<p>Сумма пополнения: <span class="amount">+{amount}</span></p>
|
||
<p>Текущий баланс: <strong>{balance}</strong></p>
|
||
</div>
|
||
<p>Спасибо за использование нашего сервиса!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Balance Successfully Topped Up!</h2>
|
||
<div class="highlight success">
|
||
<p>Top-up amount: <span class="amount">+{amount}</span></p>
|
||
<p>Current balance: <strong>{balance}</strong></p>
|
||
</div>
|
||
<p>Thank you for using our service!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'zh': f"""
|
||
<h2>充值成功!</h2>
|
||
<div class="highlight success">
|
||
<p>充值金额: <span class="amount">+{amount}</span></p>
|
||
<p>当前余额: <strong>{balance}</strong></p>
|
||
</div>
|
||
<p>感谢使用我们的服务!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'ua': f"""
|
||
<h2>Баланс успішно поповнено!</h2>
|
||
<div class="highlight success">
|
||
<p>Сума поповнення: <span class="amount">+{amount}</span></p>
|
||
<p>Поточний баланс: <strong>{balance}</strong></p>
|
||
</div>
|
||
<p>Дякуємо за використання нашого сервісу!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _balance_change_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for balance change notification."""
|
||
amount = context.get('formatted_amount', f'{context.get("amount_rubles", 0):.2f} ₽')
|
||
balance = context.get('formatted_balance', f'{context.get("new_balance_rubles", 0):.2f} ₽')
|
||
description = context.get('description', '')
|
||
|
||
subjects = {
|
||
'ru': 'Изменение баланса',
|
||
'en': 'Balance Changed',
|
||
'zh': '余额变动',
|
||
'ua': 'Зміна балансу',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Изменение баланса</h2>
|
||
<div class="highlight">
|
||
<p>Сумма: <strong>{amount}</strong></p>
|
||
<p>Текущий баланс: <strong>{balance}</strong></p>
|
||
{f'<p>Описание: {description}</p>' if description else ''}
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Balance Changed</h2>
|
||
<div class="highlight">
|
||
<p>Amount: <strong>{amount}</strong></p>
|
||
<p>Current balance: <strong>{balance}</strong></p>
|
||
{f'<p>Description: {description}</p>' if description else ''}
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
# ============================================================================
|
||
# Subscription Templates
|
||
# ============================================================================
|
||
|
||
def _subscription_expiring_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for subscription expiring notification."""
|
||
days_left = context.get('days_left', 0)
|
||
expires_at = context.get('expires_at', '')
|
||
|
||
subjects = {
|
||
'ru': f'Подписка истекает через {days_left} дн.',
|
||
'en': f'Subscription expires in {days_left} day(s)',
|
||
'zh': f'订阅将在 {days_left} 天后到期',
|
||
'ua': f'Підписка закінчується через {days_left} дн.',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Подписка скоро истекает</h2>
|
||
<div class="highlight warning">
|
||
<p>Ваша подписка истекает через <strong>{days_left}</strong> дн.</p>
|
||
<p>Дата истечения: <strong>{expires_at}</strong></p>
|
||
</div>
|
||
<p>Продлите подписку, чтобы не потерять доступ к сервису.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Subscription Expiring Soon</h2>
|
||
<div class="highlight warning">
|
||
<p>Your subscription expires in <strong>{days_left}</strong> day(s).</p>
|
||
<p>Expiration date: <strong>{expires_at}</strong></p>
|
||
</div>
|
||
<p>Renew your subscription to maintain access to our service.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'zh': f"""
|
||
<h2>订阅即将到期</h2>
|
||
<div class="highlight warning">
|
||
<p>您的订阅将在 <strong>{days_left}</strong> 天后到期。</p>
|
||
<p>到期日期: <strong>{expires_at}</strong></p>
|
||
</div>
|
||
<p>请续订以保持对服务的访问。</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'ua': f"""
|
||
<h2>Підписка скоро закінчується</h2>
|
||
<div class="highlight warning">
|
||
<p>Ваша підписка закінчується через <strong>{days_left}</strong> дн.</p>
|
||
<p>Дата закінчення: <strong>{expires_at}</strong></p>
|
||
</div>
|
||
<p>Продовжіть підписку, щоб не втратити доступ до сервісу.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _subscription_expired_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for subscription expired notification."""
|
||
subjects = {
|
||
'ru': 'Подписка истекла',
|
||
'en': 'Subscription Expired',
|
||
'zh': '订阅已到期',
|
||
'ua': 'Підписка закінчилась',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Подписка истекла</h2>
|
||
<div class="highlight danger">
|
||
<p>Ваша подписка истекла. Доступ к VPN отключён.</p>
|
||
</div>
|
||
<p>Оформите новую подписку, чтобы продолжить использование сервиса.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Subscription Expired</h2>
|
||
<div class="highlight danger">
|
||
<p>Your subscription has expired. VPN access has been disabled.</p>
|
||
</div>
|
||
<p>Purchase a new subscription to continue using our service.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'zh': f"""
|
||
<h2>订阅已到期</h2>
|
||
<div class="highlight danger">
|
||
<p>您的订阅已到期。VPN访问已被禁用。</p>
|
||
</div>
|
||
<p>请购买新订阅以继续使用我们的服务。</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'ua': f"""
|
||
<h2>Підписка закінчилась</h2>
|
||
<div class="highlight danger">
|
||
<p>Ваша підписка закінчилась. Доступ до VPN вимкнено.</p>
|
||
</div>
|
||
<p>Оформіть нову підписку, щоб продовжити використання сервісу.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _subscription_renewed_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for subscription renewed notification."""
|
||
new_expires_at = context.get('new_expires_at', '')
|
||
|
||
subjects = {
|
||
'ru': 'Подписка продлена',
|
||
'en': 'Subscription Renewed',
|
||
'zh': '订阅已续订',
|
||
'ua': 'Підписку продовжено',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Подписка успешно продлена!</h2>
|
||
<div class="highlight success">
|
||
<p>Ваша подписка была успешно продлена.</p>
|
||
<p>Новая дата истечения: <strong>{new_expires_at}</strong></p>
|
||
</div>
|
||
<p>Спасибо за использование нашего сервиса!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Subscription Successfully Renewed!</h2>
|
||
<div class="highlight success">
|
||
<p>Your subscription has been successfully renewed.</p>
|
||
<p>New expiration date: <strong>{new_expires_at}</strong></p>
|
||
</div>
|
||
<p>Thank you for using our service!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _subscription_activated_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for subscription activated notification."""
|
||
expires_at = context.get('expires_at', '')
|
||
|
||
subjects = {
|
||
'ru': 'Подписка активирована',
|
||
'en': 'Subscription Activated',
|
||
'zh': '订阅已激活',
|
||
'ua': 'Підписку активовано',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Подписка активирована!</h2>
|
||
<div class="highlight success">
|
||
<p>Ваша VPN подписка успешно активирована.</p>
|
||
<p>Действует до: <strong>{expires_at}</strong></p>
|
||
</div>
|
||
<p>Теперь вы можете пользоваться VPN сервисом.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Subscription Activated!</h2>
|
||
<div class="highlight success">
|
||
<p>Your VPN subscription has been successfully activated.</p>
|
||
<p>Valid until: <strong>{expires_at}</strong></p>
|
||
</div>
|
||
<p>You can now use the VPN service.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
# ============================================================================
|
||
# Autopay Templates
|
||
# ============================================================================
|
||
|
||
def _autopay_success_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for successful autopay notification."""
|
||
amount = context.get('formatted_amount', f'{context.get("amount_rubles", 0):.2f} ₽')
|
||
new_expires_at = context.get('new_expires_at', '')
|
||
|
||
subjects = {
|
||
'ru': 'Автопродление выполнено',
|
||
'en': 'Auto-renewal Successful',
|
||
'zh': '自动续订成功',
|
||
'ua': 'Автопродовження виконано',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Автопродление выполнено</h2>
|
||
<div class="highlight success">
|
||
<p>Ваша подписка была автоматически продлена.</p>
|
||
<p>Списано с баланса: <strong>{amount}</strong></p>
|
||
<p>Новая дата истечения: <strong>{new_expires_at}</strong></p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Auto-renewal Successful</h2>
|
||
<div class="highlight success">
|
||
<p>Your subscription has been automatically renewed.</p>
|
||
<p>Charged from balance: <strong>{amount}</strong></p>
|
||
<p>New expiration date: <strong>{new_expires_at}</strong></p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _autopay_failed_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for failed autopay notification."""
|
||
reason = context.get('reason', '')
|
||
|
||
subjects = {
|
||
'ru': 'Ошибка автопродления',
|
||
'en': 'Auto-renewal Failed',
|
||
'zh': '自动续订失败',
|
||
'ua': 'Помилка автопродовження',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Ошибка автопродления</h2>
|
||
<div class="highlight danger">
|
||
<p>Не удалось автоматически продлить подписку.</p>
|
||
{f'<p>Причина: {reason}</p>' if reason else ''}
|
||
</div>
|
||
<p>Пожалуйста, пополните баланс и продлите подписку вручную.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Auto-renewal Failed</h2>
|
||
<div class="highlight danger">
|
||
<p>Failed to automatically renew your subscription.</p>
|
||
{f'<p>Reason: {reason}</p>' if reason else ''}
|
||
</div>
|
||
<p>Please top up your balance and renew manually.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _autopay_insufficient_funds_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for autopay insufficient funds notification."""
|
||
required = context.get('required_amount', '')
|
||
balance = context.get('current_balance', '')
|
||
|
||
subjects = {
|
||
'ru': 'Недостаточно средств для автопродления',
|
||
'en': 'Insufficient Funds for Auto-renewal',
|
||
'zh': '余额不足无法自动续订',
|
||
'ua': 'Недостатньо коштів для автопродовження',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Недостаточно средств</h2>
|
||
<div class="highlight warning">
|
||
<p>Недостаточно средств на балансе для автопродления подписки.</p>
|
||
{f'<p>Требуется: <strong>{required}</strong></p>' if required else ''}
|
||
{f'<p>На балансе: <strong>{balance}</strong></p>' if balance else ''}
|
||
</div>
|
||
<p>Пополните баланс, чтобы подписка была продлена автоматически.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Insufficient Funds</h2>
|
||
<div class="highlight warning">
|
||
<p>Insufficient balance for subscription auto-renewal.</p>
|
||
{f'<p>Required: <strong>{required}</strong></p>' if required else ''}
|
||
{f'<p>Balance: <strong>{balance}</strong></p>' if balance else ''}
|
||
</div>
|
||
<p>Top up your balance for automatic renewal.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
# ============================================================================
|
||
# Daily Subscription Templates
|
||
# ============================================================================
|
||
|
||
def _daily_debit_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for daily subscription debit notification."""
|
||
amount = context.get('formatted_amount', f'{context.get("amount_rubles", 0):.2f} ₽')
|
||
balance = context.get('formatted_balance', f'{context.get("new_balance_rubles", 0):.2f} ₽')
|
||
|
||
subjects = {
|
||
'ru': f'Списание за подписку: {amount}',
|
||
'en': f'Subscription charge: {amount}',
|
||
'zh': f'订阅扣费: {amount}',
|
||
'ua': f'Списання за підписку: {amount}',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Ежедневное списание</h2>
|
||
<div class="highlight">
|
||
<p>С вашего баланса списано: <strong>{amount}</strong></p>
|
||
<p>Остаток на балансе: <strong>{balance}</strong></p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Daily Charge</h2>
|
||
<div class="highlight">
|
||
<p>Charged from your balance: <strong>{amount}</strong></p>
|
||
<p>Remaining balance: <strong>{balance}</strong></p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _daily_insufficient_funds_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for daily subscription insufficient funds."""
|
||
subjects = {
|
||
'ru': 'Недостаточно средств для продления',
|
||
'en': 'Insufficient Funds',
|
||
'zh': '余额不足',
|
||
'ua': 'Недостатньо коштів',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Недостаточно средств</h2>
|
||
<div class="highlight danger">
|
||
<p>На балансе недостаточно средств для продления подписки.</p>
|
||
<p>Подписка будет приостановлена.</p>
|
||
</div>
|
||
<p>Пополните баланс, чтобы продолжить использование сервиса.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Insufficient Funds</h2>
|
||
<div class="highlight danger">
|
||
<p>Insufficient balance to continue subscription.</p>
|
||
<p>Your subscription will be suspended.</p>
|
||
</div>
|
||
<p>Please top up your balance to continue using the service.</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _traffic_reset_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for traffic reset notification."""
|
||
subjects = {
|
||
'ru': 'Трафик обновлён',
|
||
'en': 'Traffic Reset',
|
||
'zh': '流量已重置',
|
||
'ua': 'Трафік оновлено',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Трафик обновлён</h2>
|
||
<div class="highlight success">
|
||
<p>Ваш трафик был сброшен. Вы можете продолжить использование VPN.</p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Traffic Reset</h2>
|
||
<div class="highlight success">
|
||
<p>Your traffic has been reset. You can continue using the VPN.</p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
# ============================================================================
|
||
# Account Status Templates
|
||
# ============================================================================
|
||
|
||
def _ban_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for ban notification."""
|
||
reason = context.get('reason', '')
|
||
|
||
subjects = {
|
||
'ru': 'Аккаунт заблокирован',
|
||
'en': 'Account Suspended',
|
||
'zh': '账户已被封禁',
|
||
'ua': 'Обліковий запис заблоковано',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Аккаунт заблокирован</h2>
|
||
<div class="highlight danger">
|
||
<p>Ваш аккаунт был заблокирован.</p>
|
||
{f'<p>Причина: {reason}</p>' if reason else ''}
|
||
</div>
|
||
<p>Если вы считаете, что это ошибка, обратитесь в поддержку.</p>
|
||
""",
|
||
'en': f"""
|
||
<h2>Account Suspended</h2>
|
||
<div class="highlight danger">
|
||
<p>Your account has been suspended.</p>
|
||
{f'<p>Reason: {reason}</p>' if reason else ''}
|
||
</div>
|
||
<p>If you believe this is an error, please contact support.</p>
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _unban_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for unban notification."""
|
||
subjects = {
|
||
'ru': 'Аккаунт разблокирован',
|
||
'en': 'Account Reactivated',
|
||
'zh': '账户已解封',
|
||
'ua': 'Обліковий запис розблоковано',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Аккаунт разблокирован</h2>
|
||
<div class="highlight success">
|
||
<p>Ваш аккаунт был разблокирован.</p>
|
||
<p>Вы снова можете пользоваться сервисом.</p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Account Reactivated</h2>
|
||
<div class="highlight success">
|
||
<p>Your account has been reactivated.</p>
|
||
<p>You can use the service again.</p>
|
||
</div>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _warning_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for warning notification."""
|
||
message = context.get('message', '')
|
||
|
||
subjects = {
|
||
'ru': 'Предупреждение',
|
||
'en': 'Warning',
|
||
'zh': '警告',
|
||
'ua': 'Попередження',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Предупреждение</h2>
|
||
<div class="highlight warning">
|
||
{f'<p>{message}</p>' if message else '<p>Вы получили предупреждение от администрации.</p>'}
|
||
</div>
|
||
""",
|
||
'en': f"""
|
||
<h2>Warning</h2>
|
||
<div class="highlight warning">
|
||
{f'<p>{message}</p>' if message else '<p>You have received a warning from the administration.</p>'}
|
||
</div>
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
# ============================================================================
|
||
# Referral Templates
|
||
# ============================================================================
|
||
|
||
def _referral_bonus_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for referral bonus notification."""
|
||
bonus = context.get('formatted_bonus', f'{context.get("bonus_rubles", 0):.2f} ₽')
|
||
referral_name = context.get('referral_name', '')
|
||
|
||
subjects = {
|
||
'ru': f'Реферальный бонус: +{bonus}',
|
||
'en': f'Referral bonus: +{bonus}',
|
||
'zh': f'推荐奖励: +{bonus}',
|
||
'ua': f'Реферальний бонус: +{bonus}',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Реферальный бонус!</h2>
|
||
<div class="highlight success">
|
||
<p>Вы получили реферальный бонус: <span class="amount">+{bonus}</span></p>
|
||
{f'<p>Благодаря пользователю: {referral_name}</p>' if referral_name else ''}
|
||
</div>
|
||
<p>Продолжайте приглашать друзей и зарабатывайте больше!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Referral Bonus!</h2>
|
||
<div class="highlight success">
|
||
<p>You received a referral bonus: <span class="amount">+{bonus}</span></p>
|
||
{f'<p>Thanks to: {referral_name}</p>' if referral_name else ''}
|
||
</div>
|
||
<p>Keep inviting friends and earn more!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
def _referral_registered_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for new referral registered notification."""
|
||
referral_name = context.get('referral_name', '')
|
||
|
||
subjects = {
|
||
'ru': 'Новый реферал зарегистрирован',
|
||
'en': 'New Referral Registered',
|
||
'zh': '新推荐用户已注册',
|
||
'ua': 'Новий реферал зареєстрований',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Новый реферал!</h2>
|
||
<div class="highlight success">
|
||
<p>По вашей ссылке зарегистрировался новый пользователь{f': <strong>{referral_name}</strong>' if referral_name else ''}.</p>
|
||
</div>
|
||
<p>Вы будете получать бонусы с его пополнений!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>New Referral!</h2>
|
||
<div class="highlight success">
|
||
<p>A new user registered using your link{f': <strong>{referral_name}</strong>' if referral_name else ''}.</p>
|
||
</div>
|
||
<p>You will receive bonuses from their top-ups!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
# ============================================================================
|
||
# Payment Templates
|
||
# ============================================================================
|
||
|
||
def _payment_received_template(self, language: str, context: dict[str, Any]) -> dict[str, str]:
|
||
"""Template for payment received notification."""
|
||
amount = context.get('formatted_amount', f'{context.get("amount_rubles", 0):.2f} ₽')
|
||
payment_method = context.get('payment_method', '')
|
||
|
||
subjects = {
|
||
'ru': f'Платёж получен: {amount}',
|
||
'en': f'Payment received: {amount}',
|
||
'zh': f'收到付款: {amount}',
|
||
'ua': f'Платіж отримано: {amount}',
|
||
}
|
||
|
||
bodies = {
|
||
'ru': f"""
|
||
<h2>Платёж успешно обработан</h2>
|
||
<div class="highlight success">
|
||
<p>Сумма: <span class="amount">+{amount}</span></p>
|
||
{f'<p>Способ оплаты: {payment_method}</p>' if payment_method else ''}
|
||
</div>
|
||
<p>Спасибо за оплату!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
'en': f"""
|
||
<h2>Payment Successfully Processed</h2>
|
||
<div class="highlight success">
|
||
<p>Amount: <span class="amount">+{amount}</span></p>
|
||
{f'<p>Payment method: {payment_method}</p>' if payment_method else ''}
|
||
</div>
|
||
<p>Thank you for your payment!</p>
|
||
{self._get_cabinet_button(language)}
|
||
""",
|
||
}
|
||
|
||
return {
|
||
'subject': subjects.get(language, subjects['en']),
|
||
'body_html': self._get_base_template(bodies.get(language, bodies['en'])),
|
||
}
|
||
|
||
|
||
# Singleton instance
|
||
email_notification_templates = EmailNotificationTemplates()
|