Add files via upload

This commit is contained in:
Egor
2026-01-27 01:36:18 +03:00
committed by GitHub
parent 13bb03ac91
commit 6ce42357c5
4 changed files with 245 additions and 49 deletions

View File

@@ -31,128 +31,288 @@ TEMPLATE_TYPES = [
{
'type': 'balance_topup',
'label': {'ru': 'Пополнение баланса', 'en': 'Balance Top-up', 'zh': '余额充值', 'ua': 'Поповнення балансу'},
'description': {'ru': 'Уведомление о пополнении баланса', 'en': 'Balance top-up notification', 'zh': '余额充值通知', 'ua': 'Сповіщення про поповнення балансу'},
'description': {
'ru': 'Уведомление о пополнении баланса',
'en': 'Balance top-up notification',
'zh': '余额充值通知',
'ua': 'Сповіщення про поповнення балансу',
},
'context_vars': ['amount', 'balance'],
},
{
'type': 'balance_change',
'label': {'ru': 'Изменение баланса', 'en': 'Balance Change', 'zh': '余额变动', 'ua': 'Зміна балансу'},
'description': {'ru': 'Уведомление об изменении баланса', 'en': 'Balance change notification', 'zh': '余额变动通知', 'ua': 'Сповіщення про зміну балансу'},
'description': {
'ru': 'Уведомление об изменении баланса',
'en': 'Balance change notification',
'zh': '余额变动通知',
'ua': 'Сповіщення про зміну балансу',
},
'context_vars': ['amount', 'balance'],
},
{
'type': 'subscription_expiring',
'label': {'ru': 'Подписка истекает', 'en': 'Subscription Expiring', 'zh': '订阅即将到期', 'ua': 'Підписка закінчується'},
'description': {'ru': 'Предупреждение об истечении подписки', 'en': 'Subscription expiring warning', 'zh': '订阅即将到期警告', 'ua': 'Попередження про закінчення підписки'},
'label': {
'ru': 'Подписка истекает',
'en': 'Subscription Expiring',
'zh': '订阅即将到期',
'ua': 'Підписка закінчується',
},
'description': {
'ru': 'Предупреждение об истечении подписки',
'en': 'Subscription expiring warning',
'zh': '订阅即将到期警告',
'ua': 'Попередження про закінчення підписки',
},
'context_vars': ['days_left', 'expires_at'],
},
{
'type': 'subscription_expired',
'label': {'ru': 'Подписка истекла', 'en': 'Subscription Expired', 'zh': '订阅已到期', 'ua': 'Підписка закінчилась'},
'description': {'ru': 'Уведомление об истечении подписки', 'en': 'Subscription expired notification', 'zh': '订阅已到期通知', 'ua': 'Сповіщення про закінчення підписки'},
'label': {
'ru': 'Подписка истекла',
'en': 'Subscription Expired',
'zh': '订阅已到期',
'ua': 'Підписка закінчилась',
},
'description': {
'ru': 'Уведомление об истечении подписки',
'en': 'Subscription expired notification',
'zh': '订阅已到期通知',
'ua': 'Сповіщення про закінчення підписки',
},
'context_vars': [],
},
{
'type': 'subscription_renewed',
'label': {'ru': 'Подписка продлена', 'en': 'Subscription Renewed', 'zh': '订阅已续期', 'ua': 'Підписка продовжена'},
'description': {'ru': 'Уведомление о продлении подписки', 'en': 'Subscription renewed notification', 'zh': '订阅已续期通知', 'ua': 'Сповіщення про продовження підписки'},
'label': {
'ru': 'Подписка продлена',
'en': 'Subscription Renewed',
'zh': '订阅已续期',
'ua': 'Підписка продовжена',
},
'description': {
'ru': 'Уведомление о продлении подписки',
'en': 'Subscription renewed notification',
'zh': '订阅已续期通知',
'ua': 'Сповіщення про продовження підписки',
},
'context_vars': ['new_end_date', 'tariff_name'],
},
{
'type': 'subscription_activated',
'label': {'ru': 'Подписка активирована', 'en': 'Subscription Activated', 'zh': '订阅已激活', 'ua': 'Підписка активована'},
'description': {'ru': 'Уведомление об активации подписки', 'en': 'Subscription activated notification', 'zh': '订阅已激活通知', 'ua': 'Сповіщення про активацію підписки'},
'label': {
'ru': 'Подписка активирована',
'en': 'Subscription Activated',
'zh': '订阅已激活',
'ua': 'Підписка активована',
},
'description': {
'ru': 'Уведомление об активации подписки',
'en': 'Subscription activated notification',
'zh': '订阅已激活通知',
'ua': 'Сповіщення про активацію підписки',
},
'context_vars': ['tariff_name', 'end_date'],
},
{
'type': 'autopay_success',
'label': {'ru': 'Автоплатёж успешен', 'en': 'Autopay Success', 'zh': '自动续费成功', 'ua': 'Автоплатіж успішний'},
'description': {'ru': 'Уведомление об успешном автоплатеже', 'en': 'Autopay success notification', 'zh': '自动续费成功通知', 'ua': 'Сповіщення про успішний автоплатіж'},
'label': {
'ru': 'Автоплатёж успешен',
'en': 'Autopay Success',
'zh': '自动续费成功',
'ua': 'Автоплатіж успішний',
},
'description': {
'ru': 'Уведомление об успешном автоплатеже',
'en': 'Autopay success notification',
'zh': '自动续费成功通知',
'ua': 'Сповіщення про успішний автоплатіж',
},
'context_vars': ['amount', 'balance', 'new_end_date'],
},
{
'type': 'autopay_failed',
'label': {'ru': 'Автоплатёж не удался', 'en': 'Autopay Failed', 'zh': '自动续费失败', 'ua': 'Автоплатіж не вдався'},
'description': {'ru': 'Уведомление о неудачном автоплатеже', 'en': 'Autopay failed notification', 'zh': '自动续费失败通知', 'ua': 'Сповіщення про невдалий автоплатіж'},
'label': {
'ru': 'Автоплатёж не удался',
'en': 'Autopay Failed',
'zh': '自动续费失败',
'ua': 'Автоплатіж не вдався',
},
'description': {
'ru': 'Уведомление о неудачном автоплатеже',
'en': 'Autopay failed notification',
'zh': '自动续费失败通知',
'ua': 'Сповіщення про невдалий автоплатіж',
},
'context_vars': ['reason'],
},
{
'type': 'autopay_insufficient_funds',
'label': {'ru': 'Недостаточно средств (автоплатёж)', 'en': 'Insufficient Funds (Autopay)', 'zh': '余额不足(自动续费)', 'ua': 'Недостатньо коштів (автоплатіж)'},
'description': {'ru': 'Уведомление о нехватке средств для автоплатежа', 'en': 'Insufficient funds for autopay notification', 'zh': '自动续费余额不足通知', 'ua': 'Сповіщення про нестачу коштів для автоплатежу'},
'label': {
'ru': 'Недостаточно средств (автоплатёж)',
'en': 'Insufficient Funds (Autopay)',
'zh': '余额不足(自动续费)',
'ua': 'Недостатньо коштів (автоплатіж)',
},
'description': {
'ru': 'Уведомление о нехватке средств для автоплатежа',
'en': 'Insufficient funds for autopay notification',
'zh': '自动续费余额不足通知',
'ua': 'Сповіщення про нестачу коштів для автоплатежу',
},
'context_vars': ['required_amount', 'balance'],
},
{
'type': 'daily_debit',
'label': {'ru': 'Суточное списание', 'en': 'Daily Debit', 'zh': '每日扣费', 'ua': 'Добове списання'},
'description': {'ru': 'Уведомление о суточном списании', 'en': 'Daily debit notification', 'zh': '每日扣费通知', 'ua': 'Сповіщення про добове списання'},
'description': {
'ru': 'Уведомление о суточном списании',
'en': 'Daily debit notification',
'zh': '每日扣费通知',
'ua': 'Сповіщення про добове списання',
},
'context_vars': ['amount', 'balance'],
},
{
'type': 'daily_insufficient_funds',
'label': {'ru': 'Недостаточно средств (суточное)', 'en': 'Insufficient Funds (Daily)', 'zh': '余额不足(每日)', 'ua': 'Недостатньо коштів (добове)'},
'description': {'ru': 'Уведомление о нехватке средств для суточного списания', 'en': 'Insufficient funds for daily debit', 'zh': '每日扣费余额不足通知', 'ua': 'Сповіщення про нестачу коштів для добового списання'},
'label': {
'ru': 'Недостаточно средств (суточное)',
'en': 'Insufficient Funds (Daily)',
'zh': '余额不足(每日)',
'ua': 'Недостатньо коштів (добове)',
},
'description': {
'ru': 'Уведомление о нехватке средств для суточного списания',
'en': 'Insufficient funds for daily debit',
'zh': '每日扣费余额不足通知',
'ua': 'Сповіщення про нестачу коштів для добового списання',
},
'context_vars': ['required_amount', 'balance'],
},
{
'type': 'ban_notification',
'label': {'ru': 'Блокировка аккаунта', 'en': 'Account Banned', 'zh': '账户被封禁', 'ua': 'Блокування акаунту'},
'description': {'ru': 'Уведомление о блокировке аккаунта', 'en': 'Account banned notification', 'zh': '账户被封禁通知', 'ua': 'Сповіщення про блокування акаунту'},
'description': {
'ru': 'Уведомление о блокировке аккаунта',
'en': 'Account banned notification',
'zh': '账户被封禁通知',
'ua': 'Сповіщення про блокування акаунту',
},
'context_vars': ['reason'],
},
{
'type': 'unban_notification',
'label': {'ru': 'Разблокировка аккаунта', 'en': 'Account Unbanned', 'zh': '账户已解封', 'ua': 'Розблокування акаунту'},
'description': {'ru': 'Уведомление о разблокировке аккаунта', 'en': 'Account unbanned notification', 'zh': '账户已解封通知', 'ua': 'Сповіщення про розблокування акаунту'},
'label': {
'ru': 'Разблокировка аккаунта',
'en': 'Account Unbanned',
'zh': '账户已解封',
'ua': 'Розблокування акаунту',
},
'description': {
'ru': 'Уведомление о разблокировке аккаунта',
'en': 'Account unbanned notification',
'zh': '账户已解封通知',
'ua': 'Сповіщення про розблокування акаунту',
},
'context_vars': [],
},
{
'type': 'warning_notification',
'label': {'ru': 'Предупреждение', 'en': 'Warning', 'zh': '警告', 'ua': 'Попередження'},
'description': {'ru': 'Предупреждение пользователю', 'en': 'Warning notification', 'zh': '警告通知', 'ua': 'Попередження користувачу'},
'description': {
'ru': 'Предупреждение пользователю',
'en': 'Warning notification',
'zh': '警告通知',
'ua': 'Попередження користувачу',
},
'context_vars': ['message'],
},
{
'type': 'referral_bonus',
'label': {'ru': 'Реферальный бонус', 'en': 'Referral Bonus', 'zh': '推荐奖励', 'ua': 'Реферальний бонус'},
'description': {'ru': 'Уведомление о начислении реферального бонуса', 'en': 'Referral bonus notification', 'zh': '推荐奖励通知', 'ua': 'Сповіщення про нарахування реферального бонусу'},
'description': {
'ru': 'Уведомление о начислении реферального бонуса',
'en': 'Referral bonus notification',
'zh': '推荐奖励通知',
'ua': 'Сповіщення про нарахування реферального бонусу',
},
'context_vars': ['amount', 'referral_name'],
},
{
'type': 'referral_registered',
'label': {'ru': 'Новый реферал', 'en': 'New Referral', 'zh': '新推荐用户', 'ua': 'Новий реферал'},
'description': {'ru': 'Уведомление о регистрации реферала', 'en': 'New referral registered notification', 'zh': '新推荐用户注册通知', 'ua': 'Сповіщення про реєстрацію реферала'},
'description': {
'ru': 'Уведомление о регистрации реферала',
'en': 'New referral registered notification',
'zh': '新推荐用户注册通知',
'ua': 'Сповіщення про реєстрацію реферала',
},
'context_vars': ['referral_name'],
},
{
'type': 'traffic_reset',
'label': {'ru': 'Сброс трафика', 'en': 'Traffic Reset', 'zh': '流量重置', 'ua': 'Скидання трафіку'},
'description': {'ru': 'Уведомление о сбросе трафика', 'en': 'Traffic reset notification', 'zh': '流量重置通知', 'ua': 'Сповіщення про скидання трафіку'},
'description': {
'ru': 'Уведомление о сбросе трафика',
'en': 'Traffic reset notification',
'zh': '流量重置通知',
'ua': 'Сповіщення про скидання трафіку',
},
'context_vars': ['traffic_limit'],
},
{
'type': 'payment_received',
'label': {'ru': 'Платёж получен', 'en': 'Payment Received', 'zh': '收到付款', 'ua': 'Платіж отримано'},
'description': {'ru': 'Уведомление о получении платежа', 'en': 'Payment received notification', 'zh': '收到付款通知', 'ua': 'Сповіщення про отримання платежу'},
'description': {
'ru': 'Уведомление о получении платежа',
'en': 'Payment received notification',
'zh': '收到付款通知',
'ua': 'Сповіщення про отримання платежу',
},
'context_vars': ['amount', 'payment_method'],
},
{
'type': 'email_verification',
'label': {'ru': 'Подтверждение email', 'en': 'Email Verification', 'zh': '邮箱验证', 'ua': 'Підтвердження email'},
'description': {'ru': 'Письмо для подтверждения email адреса при регистрации', 'en': 'Email address verification letter sent during registration', 'zh': '注册时发送的邮箱验证邮件', 'ua': 'Лист для підтвердження email адреси при реєстрації'},
'label': {
'ru': 'Подтверждение email',
'en': 'Email Verification',
'zh': '邮箱验证',
'ua': 'Підтвердження email',
},
'description': {
'ru': 'Письмо для подтверждения email адреса при регистрации',
'en': 'Email address verification letter sent during registration',
'zh': '注册时发送的邮箱验证邮件',
'ua': 'Лист для підтвердження email адреси при реєстрації',
},
'context_vars': ['username', 'verification_url', 'expire_hours'],
},
{
'type': 'password_reset',
'label': {'ru': 'Сброс пароля', 'en': 'Password Reset', 'zh': '重置密码', 'ua': 'Скидання пароля'},
'description': {'ru': 'Письмо для сброса пароля', 'en': 'Password reset email', 'zh': '密码重置邮件', 'ua': 'Лист для скидання пароля'},
'description': {
'ru': 'Письмо для сброса пароля',
'en': 'Password reset email',
'zh': '密码重置邮件',
'ua': 'Лист для скидання пароля',
},
'context_vars': ['username', 'reset_url', 'expire_hours'],
},
]
SAMPLE_CONTEXTS: dict[str, dict[str, Any]] = {
'balance_topup': {'formatted_amount': '500.00 ₽', 'formatted_balance': '1500.00 ₽', 'amount_rubles': 500, 'new_balance_rubles': 1500},
'balance_change': {'formatted_amount': '-200.00 ₽', 'formatted_balance': '1300.00 ₽', 'amount_rubles': -200, 'new_balance_rubles': 1300},
'balance_topup': {
'formatted_amount': '500.00 ₽',
'formatted_balance': '1500.00 ₽',
'amount_rubles': 500,
'new_balance_rubles': 1500,
},
'balance_change': {
'formatted_amount': '-200.00 ₽',
'formatted_balance': '1300.00 ₽',
'amount_rubles': -200,
'new_balance_rubles': 1300,
},
'subscription_expiring': {'days_left': 3, 'expires_at': '2025-01-30'},
'subscription_expired': {},
'subscription_renewed': {'new_end_date': '2025-02-28', 'tariff_name': 'Premium'},
@@ -169,7 +329,11 @@ SAMPLE_CONTEXTS: dict[str, dict[str, Any]] = {
'referral_registered': {'referral_name': 'John'},
'traffic_reset': {'traffic_limit': '100 GB'},
'payment_received': {'formatted_amount': '500.00 ₽', 'payment_method': 'YooKassa'},
'email_verification': {'username': 'John', 'verification_url': 'https://example.com/verify?token=abc123', 'expire_hours': 24},
'email_verification': {
'username': 'John',
'verification_url': 'https://example.com/verify?token=abc123',
'expire_hours': 24,
},
'password_reset': {'username': 'John', 'reset_url': 'https://example.com/reset?token=abc123', 'expire_hours': 1},
}
@@ -178,14 +342,17 @@ AVAILABLE_LANGUAGES = ['ru', 'en', 'zh', 'ua']
# ============ Schemas ============
class EmailTemplateUpdate(BaseModel):
"""Request to update an email template."""
subject: str = Field(..., min_length=1, max_length=500)
body_html: str = Field(..., min_length=1)
class EmailTemplatePreviewRequest(BaseModel):
"""Request to preview an email template."""
language: str = Field(default='ru')
subject: str = Field(default='')
body_html: str = Field(default='')
@@ -193,12 +360,14 @@ class EmailTemplatePreviewRequest(BaseModel):
class EmailTemplateSendTestRequest(BaseModel):
"""Request to send a test email."""
language: str = Field(default='ru')
email: str = Field(default='')
# ============ Endpoints ============
@router.get('', summary='List all email template types')
async def list_template_types(
_admin: User = Depends(get_current_admin_user),
@@ -223,10 +392,12 @@ async def list_template_types(
languages[lang] = {
'has_custom': lang in override_map.get(type_key, {}),
}
result.append({
**tpl_type,
'languages': languages,
})
result.append(
{
**tpl_type,
'languages': languages,
}
)
return {'items': result, 'available_languages': AVAILABLE_LANGUAGES}
@@ -263,6 +434,7 @@ async def get_templates_for_type(
# Get default template
try:
from app.services.notification_delivery_service import NotificationType
ntype_enum = NotificationType(notification_type)
default_template = templates_instance.get_template(ntype_enum, lang, sample_context)
except Exception:
@@ -396,6 +568,7 @@ async def preview_template(
sample_context = SAMPLE_CONTEXTS.get(notification_type, {})
try:
from app.services.notification_delivery_service import NotificationType
ntype_enum = NotificationType(notification_type)
default_template = templates_instance.get_template(ntype_enum, language, sample_context)
except Exception:
@@ -450,6 +623,7 @@ async def send_test_email(
# Check for DB override
from ..services.email_template_overrides import get_template_override
override = await get_template_override(notification_type, language, db)
if override:
@@ -458,6 +632,7 @@ async def send_test_email(
else:
try:
from app.services.notification_delivery_service import NotificationType
ntype_enum = NotificationType(notification_type)
default_template = templates_instance.get_template(ntype_enum, language, sample_context)
except Exception:

View File

@@ -421,8 +421,13 @@ async def register_email(
# Check for admin template override
override = await get_rendered_override(
'email_verification', lang,
context={'username': user.first_name or '', 'verification_url': full_url, 'expire_hours': str(expire_hours)},
'email_verification',
lang,
context={
'username': user.first_name or '',
'verification_url': full_url,
'expire_hours': str(expire_hours),
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
@@ -530,8 +535,13 @@ async def register_email_standalone(
expire_hours = settings.get_cabinet_email_verification_expire_hours()
override = await get_rendered_override(
'email_verification', lang,
context={'username': user.first_name or 'User', 'verification_url': full_url, 'expire_hours': str(expire_hours)},
'email_verification',
lang,
context={
'username': user.first_name or 'User',
'verification_url': full_url,
'expire_hours': str(expire_hours),
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
@@ -642,8 +652,13 @@ async def resend_verification(
expire_hours = settings.get_cabinet_email_verification_expire_hours()
override = await get_rendered_override(
'email_verification', lang,
context={'username': user.first_name or '', 'verification_url': full_url, 'expire_hours': str(expire_hours)},
'email_verification',
lang,
context={
'username': user.first_name or '',
'verification_url': full_url,
'expire_hours': str(expire_hours),
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
@@ -859,7 +874,8 @@ async def forgot_password(
expire_hours = settings.get_cabinet_password_reset_expire_hours()
override = await get_rendered_override(
'password_reset', lang,
'password_reset',
lang,
context={'username': user.first_name or '', 'reset_url': full_url, 'expire_hours': str(expire_hours)},
db=db,
)

View File

@@ -176,11 +176,13 @@ async def _get_available_payment_methods(
all_opts = []
for method_code in platega_methods:
info = definitions.get(method_code, {})
all_opts.append({
'id': str(method_code),
'name': info.get('title') or info.get('name') or f'Platega {method_code}',
'description': info.get('description') or info.get('name') or '',
})
all_opts.append(
{
'id': str(method_code),
'name': info.get('title') or info.get('name') or f'Platega {method_code}',
'description': info.get('description') or info.get('name') or '',
}
)
elif method_id == 'freekassa':
all_opts = [
{'id': 'sbp', 'name': '🏦 NSPK СБП', 'description': 'Система быстрых платежей'},

View File

@@ -288,6 +288,7 @@ async def get_renewal_options(
# Добавляем стоимость докупленных устройств за период продления
if extra_devices > 0 and tariff_device_price > 0:
from app.utils.pricing_utils import calculate_months_from_days
months = calculate_months_from_days(period)
price_kopeks += extra_devices * tariff_device_price * months
@@ -353,6 +354,7 @@ async def renew_subscription(
extra_devices = max(0, (user.subscription.device_limit or 0) - (tariff.device_limit or 0))
if extra_devices > 0:
from app.utils.pricing_utils import calculate_months_from_days
device_price = tariff.device_price_kopeks or settings.PRICE_PER_DEVICE
months = calculate_months_from_days(request.period_days)
price_kopeks += extra_devices * device_price * months
@@ -1491,6 +1493,7 @@ async def purchase_tariff(
effective_device_limit = existing_subscription.device_limit
if not is_daily_tariff:
from app.utils.pricing_utils import calculate_months_from_days
device_price_per_month = tariff.device_price_kopeks or settings.PRICE_PER_DEVICE
months = calculate_months_from_days(period_days)
extra_devices_cost = extra_devices * device_price_per_month * months