From 7dd67e36b3a8a408239ec53d0ae2cc230dfd726c Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 28 Mar 2026 19:12:34 +0300 Subject: [PATCH] feat: add tariff identification to all notifications for multi-tariff mode When MULTI_TARIFF_ENABLED=true users can have multiple subscriptions, so notifications must identify which tariff they relate to. - Webhook notifications: _notify_user auto-injects tariff_label from subscription.tariff.name into all 16 webhook notification strings - Monitoring service: tariff labels in expired, expiring, trial ending, follow-up waves, autopay success/failed notifications - Daily subscription: tariff in insufficient balance notification - Recurrent payments: tariff in card autopay success/failed - Auto-purchase: tariff in all 4 auto-purchase notification paths, with pre-captured names to avoid MissingGreenlet after db.commit() - Channel checker: plural form for multi-subscription deactivation - Payment providers: tariff in YooKassa and Stars activation messages - Admin: tariff in bulk expiry reminder - Localization: {tariff_label} in 20 notification keys across all 5 locales (ru, en, ua, zh, fa) + _MULTI channel keys - Fix: selectinload(Subscription.tariff) in trial expiring query - Fix: capture tariff_name before expire_subscription to prevent MissingGreenlet from db.refresh() expiring ORM relationships --- app/handlers/admin/subscriptions.py | 6 +- app/localization/locales/en.json | 46 +++++++------ app/localization/locales/fa.json | 46 +++++++------ app/localization/locales/ru.json | 46 +++++++------ app/localization/locales/ua.json | 46 +++++++------ app/localization/locales/zh.json | 46 +++++++------ app/middlewares/channel_checker.py | 31 ++++++--- app/services/daily_subscription_service.py | 5 +- app/services/monitoring_service.py | 63 +++++++++++++---- app/services/payment/stars.py | 13 +++- app/services/payment/yookassa.py | 13 +++- app/services/recurrent_payment_service.py | 4 ++ app/services/remnawave_webhook_service.py | 67 ++++++++++++++++--- .../subscription_auto_purchase_service.py | 21 ++++++ 14 files changed, 307 insertions(+), 146 deletions(-) diff --git a/app/handlers/admin/subscriptions.py b/app/handlers/admin/subscriptions.py index 892e8181..4e704db0 100644 --- a/app/handlers/admin/subscriptions.py +++ b/app/handlers/admin/subscriptions.py @@ -3,6 +3,7 @@ from aiogram import Dispatcher, F, types from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from app.config import settings from app.database.crud.subscription import ( get_all_subscriptions, get_expired_subscriptions, @@ -364,8 +365,11 @@ async def send_expiry_reminders(callback: types.CallbackQuery, db_user: User, db days_left = max(1, subscription.days_left) + tariff_label = '' + if settings.is_multi_tariff_enabled() and hasattr(subscription, 'tariff') and subscription.tariff: + tariff_label = f' «{subscription.tariff.name}»' reminder_text = f""" -⚠️ Подписка истекает! +⚠️ Подписка{tariff_label} истекает! Ваша подписка истекает через {days_left} день(а). diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 387dca10..b72fcff0 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -1437,11 +1437,11 @@ "SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "Step 3 - Connect:", "SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "Step 1 - Install:", "SUBSCRIPTION_EXPIRED": "\n❌ Subscription expired\n\nYour subscription has ended. Renew it to restore access.\n", - "SUBSCRIPTION_EXPIRED_1D": "⛔ Your subscription expired\n\nAccess was disabled on {end_date}. Renew to return to the service.\n\n💎 Renewal price: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 {percent}% discount on renewal\n\nActivate the offer to get an additional discount. It stacks with your promo group and is valid until {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount\n\nIt's been {trigger_days} days without a subscription. Come back and activate the extra discount — it stacks with your promo group and is valid until {expires_at}.", + "SUBSCRIPTION_EXPIRED_1D": "⛔ Your subscription{tariff_label} expired\n\nAccess was disabled on {end_date}. Renew to return to the service.\n\n💎 Renewal price: {price}", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 {percent}% discount on renewal{tariff_label}\n\nActivate the offer to get an additional discount. It stacks with your promo group and is valid until {expires_at}.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount{tariff_label}\n\nIt's been {trigger_days} days without a subscription. Come back and activate the extra discount — it stacks with your promo group and is valid until {expires_at}.", "SUBSCRIPTION_EXPIRING": "\n⚠️ Subscription expiring!\n\nYour subscription expires in {days} days.\n\nRenew it now so you don't lose access.\n", - "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ Subscription expires in {days_text}!\n\nYour paid subscription ends on {end_date}.\n\n💳 Autopay: {autopay_status}\n\n{action_text}\n", + "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ Subscription{tariff_label} expires in {days_text}!\n\nYour paid subscription ends on {end_date}.\n\n💳 Autopay: {autopay_status}\n\n{action_text}\n", "SUBSCRIPTION_EXTEND": "💎 Extend", "SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "
{crypto_link}
", "SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 Subscription link is ready. Tap the \"Connect\" button below to open it in Happ.", @@ -1467,12 +1467,14 @@ "SUBSCRIPTION_NO_SERVERS": "No servers", "SUBSCRIPTION_OVERVIEW_TEMPLATE": "👤 {full_name}\n💰 Balance: {balance}\n📱 Subscription: {status_emoji} {status_display}{warning}{tariff_info_block}\n\n📱 Subscription details\n🎭 Type: {subscription_type}\n📅 Valid until: {end_date}\n⏰ Time left: {time_left}\n📈 Traffic: {traffic}\n🌍 Servers: {servers}\n📱 Devices: {devices_used} / {device_limit}", "SUBSCRIPTION_DAILY_OVERVIEW_TEMPLATE": "👤 {full_name}\n💰 Balance: {balance}\n📱 Subscription: {status_emoji} {status_display}{warning}{tariff_info_block}\n\n📱 Subscription details\n🎭 Type: {subscription_type}\n📈 Traffic: {traffic}\n🌍 Servers: {servers}\n📱 Devices: {devices_used} / {device_limit}", - "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "Your subscription has been paused because you left a required channel.\n\nSubscribe to all channels to restore VPN access.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "🚫 Your subscription has been paused because you left a required channel.\n\nSubscribe to all channels to restore VPN access.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI": "🚫 Your subscriptions have been paused because you left a required channel.\n\nSubscribe to all channels to restore VPN access.", "SUBSCRIPTION_PROMO_DISCOUNT_HINT": "⚡ Extra {percent}% discount is active and will apply automatically. It stacks with other discounts.", "SUBSCRIPTION_PROMO_DISCOUNT_NOTE": "⚡ Extra discount {percent}%: -{amount}", "SUBSCRIPTION_PROMO_DISCOUNT_TIMER": "⏳ Discount active for {time_left}\n{bar}", "SUBSCRIPTION_PURCHASED": "🎉 Subscription purchased successfully!", - "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "Your subscription has been restored!\n\nThank you for subscribing to the channels. VPN is active again.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "✅ Your subscription has been restored!\n\nThank you for subscribing to the channels. VPN is active again.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE_MULTI": "✅ Your subscriptions have been restored!\n\nThank you for subscribing to the channels. VPN is active again.", "SUBSCRIPTION_SETTINGS_BUTTON": "⚙️ Settings", "SUBSCRIPTION_SETTINGS_OVERVIEW": "⚙️ Subscription settings\n\n📊 Current parameters:\n🌐 Countries: {countries_count}\n📈 Traffic: {traffic_used} / {traffic_limit}\n📱 Devices: {devices_used} / {devices_limit}\n\nChoose what you want to change:", "SUBSCRIPTION_SETTINGS_PAID_ONLY": "⚠️ Settings are available only for paid subscriptions", @@ -1736,22 +1738,22 @@ "DAILY_SUBSCRIPTION_PAUSED": "⏸️ Subscription paused", "DAILY_SUBSCRIPTION_RESUMED": "▶️ Subscription resumed!", "DAILY_SUBSCRIPTION_RESUMED_AFTER_TOPUP": "✅ Subscription resumed!\n\nYour daily plan «{tariff_name}» has been resumed after balance top-up.\n\n💳 Charged: {amount}\n💰 Remaining: {balance}", - "WEBHOOK_SUB_EXPIRED": "❌ Subscription expired\n\nYour subscription has ended. Renew to restore VPN access.", - "WEBHOOK_SUB_DISABLED": "🚫 Subscription disabled\n\nYour subscription has been disabled by the administrator.", - "WEBHOOK_SUB_ENABLED": "✅ Subscription activated\n\nYour subscription is active again. Enjoy!", - "WEBHOOK_SUB_LIMITED": "⚠️ Traffic limit reached\n\nYou have used all available traffic. Purchase additional traffic or wait for a reset.", - "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 Traffic reset\n\nYour traffic has been reset. Full limit is available again.", - "WEBHOOK_SUB_DELETED": "🗑️ Subscription deleted\n\nYour subscription was deleted from the panel.", - "WEBHOOK_SUB_REVOKED": "🔑 Subscription key updated\n\nYour connection credentials have been updated. Use the new link to connect.", - "WEBHOOK_SUB_EXPIRES_72H": "⏳ Subscription expires in 3 days\n\nDon't forget to renew your subscription to keep VPN access.", - "WEBHOOK_SUB_EXPIRES_48H": "⏳ Subscription expires in 2 days\n\nRenew your subscription in advance to maintain VPN access.", - "WEBHOOK_SUB_EXPIRES_24H": "🔴 Subscription expires tomorrow!\n\nLess than 24 hours left. Renew your subscription now.", - "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 Subscription expired yesterday\n\nYour subscription has ended. Renew now to restore VPN access.", - "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 First connection!\n\nYou connected to the VPN for the first time. Welcome!", - "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 {percent}% traffic used\n\nYou have used a significant portion of your traffic. Monitor your usage.", + "WEBHOOK_SUB_EXPIRED": "❌ Subscription{tariff_label} expired\n\nYour subscription has ended. Renew to restore VPN access.", + "WEBHOOK_SUB_DISABLED": "🚫 Subscription{tariff_label} disabled\n\nYour subscription has been disabled by the administrator.", + "WEBHOOK_SUB_ENABLED": "✅ Subscription{tariff_label} activated\n\nYour subscription is active again. Enjoy!", + "WEBHOOK_SUB_LIMITED": "⚠️ Traffic limit reached\n\nYou have used all available traffic for your subscription{tariff_label}. Purchase additional traffic or wait for a reset.", + "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 Traffic reset\n\nTraffic for your subscription{tariff_label} has been reset. Full limit is available again.", + "WEBHOOK_SUB_DELETED": "🗑️ Subscription{tariff_label} deleted\n\nYour subscription was deleted from the panel.", + "WEBHOOK_SUB_REVOKED": "🔑 Subscription{tariff_label} key updated\n\nYour connection credentials have been updated. Use the new link to connect.", + "WEBHOOK_SUB_EXPIRES_72H": "⏳ Subscription{tariff_label} expires in 3 days\n\nDon't forget to renew your subscription to keep VPN access.", + "WEBHOOK_SUB_EXPIRES_48H": "⏳ Subscription{tariff_label} expires in 2 days\n\nRenew your subscription in advance to maintain VPN access.", + "WEBHOOK_SUB_EXPIRES_24H": "🔴 Subscription{tariff_label} expires tomorrow!\n\nLess than 24 hours left. Renew your subscription now.", + "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 Subscription{tariff_label} expired yesterday\n\nYour subscription has ended. Renew now to restore VPN access.", + "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 First connection!\n\nYou connected to the VPN{tariff_label} for the first time. Welcome!", + "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 {percent}% traffic used\n\nYou have used a significant portion of traffic for your subscription{tariff_label}. Monitor your usage.", "WEBHOOK_RENEW_BUTTON": "🔄 Renew subscription", - "WEBHOOK_USER_NOT_CONNECTED": "📡 Not connected yet\n\nYour subscription is active but no VPN connection has been made. Connect to start using the service.", - "WEBHOOK_DEVICE_ADDED": "📱 New device\n\nA new device has been added to your subscription: {device}", - "WEBHOOK_DEVICE_DELETED": "📱 Device removed\n\nA device has been removed from your subscription: {device}", + "WEBHOOK_USER_NOT_CONNECTED": "📡 Not connected yet\n\nYour subscription{tariff_label} is active but no VPN connection has been made. Connect to start using the service.", + "WEBHOOK_DEVICE_ADDED": "📱 New device\n\nA new device has been added to your subscription{tariff_label}: {device}", + "WEBHOOK_DEVICE_DELETED": "📱 Device removed\n\nA device has been removed from your subscription{tariff_label}: {device}", "WEBHOOK_CLOSE_BUTTON": "✖️ Close" } \ No newline at end of file diff --git a/app/localization/locales/fa.json b/app/localization/locales/fa.json index 2eab3b7a..d560721b 100644 --- a/app/localization/locales/fa.json +++ b/app/localization/locales/fa.json @@ -1458,11 +1458,11 @@ "SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "مرحله ۳ - اتصال:", "SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "مرحله ۱ - نصب:", "SUBSCRIPTION_EXPIRED": "❌ اشتراک منقضی شده\n\nبرای ادامه تمدید کنید.", - "SUBSCRIPTION_EXPIRED_1D": "❌ اشتراک دیروز منقضی شد. تمدید کنید!", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 تخفیف {percent}% برای تمدید\n\nپیشنهاد را فعال کنید تا تخفیف اضافی بگیرید. با گروه تخفیف جمع شده و تا {expires_at} اعتبار دارد.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 تخفیف اختصاصی {percent}%\n\n{trigger_days} روز بدون اشتراک گذشته. برگردید و تخفیف اضافی فعال کنید — با گروه تخفیف جمع شده و تا {expires_at} اعتبار دارد.", + "SUBSCRIPTION_EXPIRED_1D": "⛔ اشتراک{tariff_label} منقضی شد\n\nدسترسی در {end_date} قطع شده است. برای بازگشت به سرویس اشتراک را تمدید کنید.\n\n💎 هزینه تمدید: {price}", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 تخفیف {percent}% برای تمدید{tariff_label}\n\nپیشنهاد را فعال کنید تا تخفیف اضافی بگیرید. با گروه تخفیف جمع شده و تا {expires_at} اعتبار دارد.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 تخفیف اختصاصی {percent}%{tariff_label}\n\n{trigger_days} روز بدون اشتراک گذشته. برگردید و تخفیف اضافی فعال کنید — با گروه تخفیف جمع شده و تا {expires_at} اعتبار دارد.", "SUBSCRIPTION_EXPIRING": "⏰ اشتراک به‌زودی منقضی می‌شود!", - "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ اشتراک تا {days_text} دیگر منقضی می‌شود!\n\nاشتراک پولی شما در تاریخ {end_date} منقضی می‌شود.\n\n💳 پرداخت خودکار: {autopay_status}\n\n{action_text}\n", + "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ اشتراک{tariff_label} تا {days_text} دیگر منقضی می‌شود!\n\nاشتراک پولی شما در تاریخ {end_date} منقضی می‌شود.\n\n💳 پرداخت خودکار: {autopay_status}\n\n{action_text}\n", "SUBSCRIPTION_EXTEND": "🔄 تمدید اشتراک", "SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "
{crypto_link}
", "SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 لینک اشتراک ایجاد شد. دکمه «اتصال» بزنید تا در Happ باز شود.", @@ -1488,12 +1488,14 @@ "SUBSCRIPTION_NO_SERVERS": "❌ سروری در دسترس نیست", "SUBSCRIPTION_OVERVIEW_TEMPLATE": "📱 اشتراک شما\n\n📊 وضعیت: {status}\n📅 اعتبار: {expiry}\n📈 ترافیک: {traffic}\n📱 دستگاه‌ها: {devices}\n🌍 سرورها: {servers}", "SUBSCRIPTION_DAILY_OVERVIEW_TEMPLATE": "👤 {full_name}\n💰 موجودی: {balance}\n📱 اشتراک: {status_emoji} {status_display}{warning}{tariff_info_block}\n\n📱 اطلاعات اشتراک\n🎭 نوع: {subscription_type}\n📈 ترافیک: {traffic}\n🌍 سرورها: {servers}\n📱 دستگاه‌ها: {devices}\n🌍 کشورها: {countries}", - "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "اشتراک شما متوقف شده است زیرا از کانال اجباری خارج شدید.\n\nبرای بازیابی دسترسی VPN در همه کانال‌ها عضو شوید.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "🚫 اشتراک شما متوقف شده است زیرا از کانال اجباری خارج شدید.\n\nبرای بازیابی دسترسی VPN در همه کانال‌ها عضو شوید.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI": "🚫 اشتراک‌های شما متوقف شده‌اند زیرا از کانال اجباری خارج شدید.\n\nبرای بازیابی دسترسی VPN در همه کانال‌ها عضو شوید.", "SUBSCRIPTION_PROMO_DISCOUNT_HINT": "⚡ تخفیف اضافی {percent}% فعال شد.\n\nبا سایر تخفیف‌ها جمع می‌شود!", "SUBSCRIPTION_PROMO_DISCOUNT_NOTE": "⚡ تخفیف اضافی {percent}%: -{amount}", "SUBSCRIPTION_PROMO_DISCOUNT_TIMER": "\n⏳ اعتبار تخفیف: {time_left} {bar}", "SUBSCRIPTION_PURCHASED": "🎉 اشتراک خریداری شد!", - "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "اشتراک شما بازیابی شد!\n\nبا تشکر از عضویت در کانال‌ها. VPN دوباره فعال است.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "✅ اشتراک شما بازیابی شد!\n\nبا تشکر از عضویت در کانال‌ها. VPN دوباره فعال است.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE_MULTI": "✅ اشتراک‌های شما بازیابی شدند!\n\nبا تشکر از عضویت در کانال‌ها. VPN دوباره فعال است.", "SUBSCRIPTION_SETTINGS_BUTTON": "⚙️ تنظیمات", "SUBSCRIPTION_SETTINGS_OVERVIEW": "⚙️ تنظیمات اشتراک", "SUBSCRIPTION_SETTINGS_PAID_ONLY": "⚠️ تنظیمات فقط برای اشتراک پولی", @@ -1757,22 +1759,22 @@ "DAILY_SUBSCRIPTION_PAUSED": "⏸️ اشتراک متوقف شد", "DAILY_SUBSCRIPTION_RESUMED": "▶️ اشتراک از سر گرفته شد!", "DAILY_SUBSCRIPTION_RESUMED_AFTER_TOPUP": "✅ اشتراک از سر گرفته شد!\n\nتعرفه روزانه «{tariff_name}» پس از شارژ موجودی از سر گرفته شد.\n\n💳 کسر شده: {amount}\n💰 باقی‌مانده: {balance}", - "WEBHOOK_SUB_EXPIRED": "❌ اشتراک منقضی شد\n\nاشتراک شما به پایان رسیده است. برای بازیابی دسترسی VPN تمدید کنید.", - "WEBHOOK_SUB_DISABLED": "🚫 اشتراک غیرفعال شد\n\nاشتراک شما توسط مدیر غیرفعال شده است.", - "WEBHOOK_SUB_ENABLED": "✅ اشتراک فعال شد\n\nاشتراک شما دوباره فعال است. از استفاده لذت ببرید!", - "WEBHOOK_SUB_LIMITED": "⚠️ محدودیت ترافیک\n\nشما تمام ترافیک موجود را مصرف کرده‌اید. ترافیک اضافی بخرید یا منتظر بازنشانی باشید.", - "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 ترافیک بازنشانی شد\n\nترافیک شما بازنشانی شد. محدودیت کامل دوباره در دسترس است.", - "WEBHOOK_SUB_DELETED": "🗑️ اشتراک حذف شد\n\nاشتراک شما از پنل حذف شده است.", - "WEBHOOK_SUB_REVOKED": "🔑 کلید اشتراک به‌روز شد\n\nاطلاعات اتصال شما به‌روز شده است. از لینک جدید برای اتصال استفاده کنید.", - "WEBHOOK_SUB_EXPIRES_72H": "⏳ اشتراک تا ۳ روز دیگر منقضی می‌شود\n\nفراموش نکنید اشتراک خود را تمدید کنید.", - "WEBHOOK_SUB_EXPIRES_48H": "⏳ اشتراک تا ۲ روز دیگر منقضی می‌شود\n\nاشتراک خود را زودتر تمدید کنید تا دسترسی VPN حفظ شود.", - "WEBHOOK_SUB_EXPIRES_24H": "🔴 اشتراک فردا منقضی می‌شود!\n\nکمتر از ۲۴ ساعت باقی مانده. همین الان تمدید کنید.", - "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 اشتراک دیروز منقضی شد\n\nاشتراک شما به پایان رسیده. همین الان تمدید کنید تا دسترسی VPN بازگردد.", - "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 اولین اتصال!\n\nشما برای اولین بار به VPN متصل شدید. خوش آمدید!", - "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 {percent}% ترافیک مصرف شد\n\nبخش قابل توجهی از ترافیک خود را مصرف کرده‌اید. مصرف خود را زیر نظر داشته باشید.", + "WEBHOOK_SUB_EXPIRED": "❌ اشتراک{tariff_label} منقضی شد\n\nاشتراک شما به پایان رسیده است. برای بازیابی دسترسی VPN تمدید کنید.", + "WEBHOOK_SUB_DISABLED": "🚫 اشتراک{tariff_label} غیرفعال شد\n\nاشتراک شما توسط مدیر غیرفعال شده است.", + "WEBHOOK_SUB_ENABLED": "✅ اشتراک{tariff_label} فعال شد\n\nاشتراک شما دوباره فعال است. از استفاده لذت ببرید!", + "WEBHOOK_SUB_LIMITED": "⚠️ محدودیت ترافیک\n\nشما تمام ترافیک موجود اشتراک{tariff_label} را مصرف کرده‌اید. ترافیک اضافی بخرید یا منتظر بازنشانی باشید.", + "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 ترافیک بازنشانی شد\n\nترافیک اشتراک{tariff_label} بازنشانی شد. محدودیت کامل دوباره در دسترس است.", + "WEBHOOK_SUB_DELETED": "🗑️ اشتراک{tariff_label} حذف شد\n\nاشتراک شما از پنل حذف شده است.", + "WEBHOOK_SUB_REVOKED": "🔑 کلید اشتراک{tariff_label} به‌روز شد\n\nاطلاعات اتصال شما به‌روز شده است. از لینک جدید برای اتصال استفاده کنید.", + "WEBHOOK_SUB_EXPIRES_72H": "⏳ اشتراک{tariff_label} تا ۳ روز دیگر منقضی می‌شود\n\nفراموش نکنید اشتراک خود را تمدید کنید.", + "WEBHOOK_SUB_EXPIRES_48H": "⏳ اشتراک{tariff_label} تا ۲ روز دیگر منقضی می‌شود\n\nاشتراک خود را زودتر تمدید کنید تا دسترسی VPN حفظ شود.", + "WEBHOOK_SUB_EXPIRES_24H": "🔴 اشتراک{tariff_label} فردا منقضی می‌شود!\n\nکمتر از ۲۴ ساعت باقی مانده. همین الان تمدید کنید.", + "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 اشتراک{tariff_label} دیروز منقضی شد\n\nاشتراک شما به پایان رسیده. همین الان تمدید کنید تا دسترسی VPN بازگردد.", + "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 اولین اتصال!\n\nشما برای اولین بار به VPN{tariff_label} متصل شدید. خوش آمدید!", + "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 {percent}% ترافیک مصرف شد\n\nبخش قابل توجهی از ترافیک اشتراک{tariff_label} خود را مصرف کرده‌اید. مصرف خود را زیر نظر داشته باشید.", "WEBHOOK_RENEW_BUTTON": "🔄 تمدید اشتراک", - "WEBHOOK_USER_NOT_CONNECTED": "📡 هنوز متصل نشده‌اید\n\nاشتراک شما فعال است اما هنوز اتصال VPN برقرار نشده. برای شروع استفاده متصل شوید.", - "WEBHOOK_DEVICE_ADDED": "📱 دستگاه جدید\n\nدستگاه جدیدی به اشتراک شما اضافه شد: {device}", - "WEBHOOK_DEVICE_DELETED": "📱 دستگاه حذف شد\n\nدستگاهی از اشتراک شما حذف شد: {device}", + "WEBHOOK_USER_NOT_CONNECTED": "📡 هنوز متصل نشده‌اید\n\nاشتراک{tariff_label} شما فعال است اما هنوز اتصال VPN برقرار نشده. برای شروع استفاده متصل شوید.", + "WEBHOOK_DEVICE_ADDED": "📱 دستگاه جدید\n\nدستگاه جدیدی به اشتراک{tariff_label} شما اضافه شد: {device}", + "WEBHOOK_DEVICE_DELETED": "📱 دستگاه حذف شد\n\nدستگاهی از اشتراک{tariff_label} شما حذف شد: {device}", "WEBHOOK_CLOSE_BUTTON": "✖️ بستن" } \ No newline at end of file diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 5e9e423d..16b4f2de 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -1458,11 +1458,11 @@ "SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "Шаг 3 - Подключение:", "SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "Шаг 1 - Установка:", "SUBSCRIPTION_EXPIRED": "\n❌ Подписка истекла\n\nВаша подписка истекла. Для восстановления доступа продлите подписку.\n", - "SUBSCRIPTION_EXPIRED_1D": "⛔ Подписка закончилась\n\nДоступ был отключён {end_date}. Продлите подписку, чтобы вернуть полный доступ.\n\n💎 Стоимость продления: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Скидка {percent}% на продление\n\nАктивируйте предложение, чтобы получить дополнительную скидку. Она суммируется с вашей промогруппой и действует до {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%\n\nПрошло {trigger_days} дней без подписки. Вернитесь и активируйте дополнительную скидку — она суммируется с промогруппой и действует до {expires_at}.", + "SUBSCRIPTION_EXPIRED_1D": "⛔ Подписка{tariff_label} закончилась\n\nДоступ был отключён {end_date}. Продлите подписку, чтобы вернуть полный доступ.\n\n💎 Стоимость продления: {price}", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Скидка {percent}% на продление{tariff_label}\n\nАктивируйте предложение, чтобы получить дополнительную скидку. Она суммируется с вашей промогруппой и действует до {expires_at}.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%{tariff_label}\n\nПрошло {trigger_days} дней без подписки. Вернитесь и активируйте дополнительную скидку — она суммируется с промогруппой и действует до {expires_at}.", "SUBSCRIPTION_EXPIRING": "\n⚠️ Подписка истекает!\n\nВаша подписка истекает через {days} дней.\n\nНе забудьте продлить подписку, чтобы не потерять доступ к серверам.\n", - "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ Подписка истекает через {days_text}!\n\nВаша платная подписка истекает {end_date}.\n\n💳 Автоплатеж: {autopay_status}\n\n{action_text}\n", + "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ Подписка{tariff_label} истекает через {days_text}!\n\nВаша платная подписка истекает {end_date}.\n\n💳 Автоплатеж: {autopay_status}\n\n{action_text}\n", "SUBSCRIPTION_EXTEND": "💎 Продлить", "SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "
{crypto_link}
", "SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.", @@ -1488,12 +1488,14 @@ "SUBSCRIPTION_NO_SERVERS": "Нет серверов", "SUBSCRIPTION_OVERVIEW_TEMPLATE": "👤 {full_name}\n💰 Баланс: {balance}\n📱 Подписка: {status_emoji} {status_display}{warning}{tariff_info_block}\n\n📱 Информация о подписке\n🎭 Тип: {subscription_type}\n📅 Действует до: {end_date}\n⏰ Осталось: {time_left}\n📈 Трафик: {traffic}\n🌍 Серверы: {servers}\n📱 Устройства: {devices_used} / {device_limit}", "SUBSCRIPTION_DAILY_OVERVIEW_TEMPLATE": "👤 {full_name}\n💰 Баланс: {balance}\n📱 Подписка: {status_emoji} {status_display}{warning}{tariff_info_block}\n\n📱 Информация о подписке\n🎭 Тип: {subscription_type}\n📈 Трафик: {traffic}\n🌍 Серверы: {servers}\n📱 Устройства: {devices_used} / {device_limit}", - "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "Ваша подписка приостановлена, так как вы отписались от обязательного канала.\n\nПодпишитесь на все каналы для восстановления доступа к VPN.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "🚫 Ваша подписка приостановлена, так как вы отписались от обязательного канала.\n\nПодпишитесь на все каналы для восстановления доступа к VPN.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI": "🚫 Ваши подписки приостановлены, так как вы отписались от обязательного канала.\n\nПодпишитесь на все каналы для восстановления доступа к VPN.", "SUBSCRIPTION_PROMO_DISCOUNT_HINT": "⚡ Активирована доп. скидка {percent}%. \n\nСуммируется с другими скидками!", "SUBSCRIPTION_PROMO_DISCOUNT_NOTE": "⚡ Доп. скидка {percent}%: -{amount}", "SUBSCRIPTION_PROMO_DISCOUNT_TIMER": "⏳ Скидка действует ещё: {time_left}\n{bar}", "SUBSCRIPTION_PURCHASED": "🎉 Подписка успешно приобретена!", - "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "Ваша подписка восстановлена!\n\nСпасибо за подписку на каналы. VPN снова активен.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "✅ Ваша подписка восстановлена!\n\nСпасибо за подписку на каналы. VPN снова активен.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE_MULTI": "✅ Ваши подписки восстановлены!\n\nСпасибо за подписку на каналы. VPN снова активен.", "SUBSCRIPTION_SETTINGS_BUTTON": "⚙️ Настройки", "SUBSCRIPTION_SETTINGS_OVERVIEW": "⚙️ Настройки подписки\n\n📊 Текущие параметры:\n🌐 Стран: {countries_count}\n📈 Трафик: {traffic_used} / {traffic_limit}\n📱 Устройства: {devices_used} / {devices_limit}\n\nВыберите что хотите изменить:", "SUBSCRIPTION_SETTINGS_PAID_ONLY": "⚠️ Настройки доступны только для платных подписок", @@ -1760,22 +1762,22 @@ "DAILY_SUBSCRIPTION_RESUMED": "▶️ Подписка возобновлена!", "DAILY_SUBSCRIPTION_RESUMED_AFTER_TOPUP": "✅ Подписка возобновлена!\n\nВаш суточный тариф «{tariff_name}» возобновлён после пополнения баланса.\n\n💳 Списано: {amount}\n💰 Остаток: {balance}", - "WEBHOOK_SUB_EXPIRED": "❌ Подписка истекла\n\nВаша подписка завершена. Продлите подписку, чтобы восстановить доступ к VPN.", - "WEBHOOK_SUB_DISABLED": "🚫 Подписка отключена\n\nВаша подписка была отключена администратором.", - "WEBHOOK_SUB_ENABLED": "✅ Подписка активирована\n\nВаша подписка снова активна. Приятного использования!", - "WEBHOOK_SUB_LIMITED": "⚠️ Достигнут лимит трафика\n\nВы исчерпали весь доступный трафик. Докупите трафик или дождитесь сброса.", - "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 Трафик сброшен\n\nВаш трафик обнулён. Лимит снова доступен в полном объёме.", - "WEBHOOK_SUB_DELETED": "🗑️ Подписка удалена\n\nВаша подписка была удалена из панели.", - "WEBHOOK_SUB_REVOKED": "🔑 Ключ подписки обновлён\n\nВаши данные подключения были обновлены. Используйте новую ссылку для подключения.", - "WEBHOOK_SUB_EXPIRES_72H": "⏳ Подписка истекает через 3 дня\n\nНе забудьте продлить подписку, чтобы не потерять доступ.", - "WEBHOOK_SUB_EXPIRES_48H": "⏳ Подписка истекает через 2 дня\n\nПродлите подписку заранее, чтобы сохранить доступ к VPN.", - "WEBHOOK_SUB_EXPIRES_24H": "🔴 Подписка истекает завтра!\n\nОсталось менее 24 часов. Продлите подписку прямо сейчас.", - "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 Подписка истекла вчера\n\nВаша подписка завершена. Продлите сейчас, чтобы вернуть доступ к VPN.", - "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 Первое подключение!\n\nВы впервые подключились к VPN. Добро пожаловать!", - "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 Использован {percent}% трафика\n\nВы использовали значительную часть трафика. Следите за расходом.", + "WEBHOOK_SUB_EXPIRED": "❌ Подписка{tariff_label} истекла\n\nВаша подписка завершена. Продлите подписку, чтобы восстановить доступ к VPN.", + "WEBHOOK_SUB_DISABLED": "🚫 Подписка{tariff_label} отключена\n\nВаша подписка была отключена администратором.", + "WEBHOOK_SUB_ENABLED": "✅ Подписка{tariff_label} активирована\n\nВаша подписка снова активна. Приятного использования!", + "WEBHOOK_SUB_LIMITED": "⚠️ Достигнут лимит трафика\n\nВы исчерпали весь доступный трафик по подписке{tariff_label}. Докупите трафик или дождитесь сброса.", + "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 Трафик сброшен\n\nТрафик по подписке{tariff_label} обнулён. Лимит снова доступен в полном объёме.", + "WEBHOOK_SUB_DELETED": "🗑️ Подписка{tariff_label} удалена\n\nВаша подписка была удалена из панели.", + "WEBHOOK_SUB_REVOKED": "🔑 Ключ подписки{tariff_label} обновлён\n\nВаши данные подключения были обновлены. Используйте новую ссылку для подключения.", + "WEBHOOK_SUB_EXPIRES_72H": "⏳ Подписка{tariff_label} истекает через 3 дня\n\nНе забудьте продлить подписку, чтобы не потерять доступ.", + "WEBHOOK_SUB_EXPIRES_48H": "⏳ Подписка{tariff_label} истекает через 2 дня\n\nПродлите подписку заранее, чтобы сохранить доступ к VPN.", + "WEBHOOK_SUB_EXPIRES_24H": "🔴 Подписка{tariff_label} истекает завтра!\n\nОсталось менее 24 часов. Продлите подписку прямо сейчас.", + "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 Подписка{tariff_label} истекла вчера\n\nВаша подписка завершена. Продлите сейчас, чтобы вернуть доступ к VPN.", + "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 Первое подключение!\n\nВы впервые подключились к VPN{tariff_label}. Добро пожаловать!", + "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 Использован {percent}% трафика\n\nВы использовали значительную часть трафика по подписке{tariff_label}. Следите за расходом.", "WEBHOOK_RENEW_BUTTON": "🔄 Продлить подписку", - "WEBHOOK_USER_NOT_CONNECTED": "📡 Вы ещё не подключились\n\nВаша подписка активна, но VPN-соединение не было установлено. Подключитесь, чтобы начать пользоваться.", - "WEBHOOK_DEVICE_ADDED": "📱 Новое устройство\n\nК вашей подписке подключено новое устройство: {device}", - "WEBHOOK_DEVICE_DELETED": "📱 Устройство удалено\n\nУстройство отключено от подписки: {device}", + "WEBHOOK_USER_NOT_CONNECTED": "📡 Вы ещё не подключились\n\nВаша подписка{tariff_label} активна, но VPN-соединение не было установлено. Подключитесь, чтобы начать пользоваться.", + "WEBHOOK_DEVICE_ADDED": "📱 Новое устройство\n\nК подписке{tariff_label} подключено новое устройство: {device}", + "WEBHOOK_DEVICE_DELETED": "📱 Устройство удалено\n\nУстройство отключено от подписки{tariff_label}: {device}", "WEBHOOK_CLOSE_BUTTON": "✖️ Закрыть" } diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json index 9b24c1f3..0a27e3a8 100644 --- a/app/localization/locales/ua.json +++ b/app/localization/locales/ua.json @@ -1368,11 +1368,11 @@ "SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "Крок 3 - Підключення:", "SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "Крок 1 - Встановлення:", "SUBSCRIPTION_EXPIRED": "\n❌ Підписка закінчилася\n\nВаша підписка закінчилася. Для відновлення доступу продовжіть підписку.\n", - "SUBSCRIPTION_EXPIRED_1D": "⛔ Підписка закінчилася\n\nДоступ було вимкнено {end_date}. Продовжіть підписку, щоб повернути повний доступ.\n\n💎 Вартість продовження: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Знижка {percent}% на продовження\n\nАктивуйте пропозицію, щоб отримати додаткову знижку. Вона сумується з вашою промогрупою та діє до {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Індивідуальна знижка {percent}%\n\nМинуло {trigger_days} днів без підписки. Поверніться та активуйте додаткову знижку — вона сумується з промогрупою та діє до {expires_at}.", + "SUBSCRIPTION_EXPIRED_1D": "⛔ Підписка{tariff_label} закінчилася\n\nДоступ було вимкнено {end_date}. Продовжіть підписку, щоб повернути повний доступ.\n\n💎 Вартість продовження: {price}", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Знижка {percent}% на продовження{tariff_label}\n\nАктивуйте пропозицію, щоб отримати додаткову знижку. Вона сумується з вашою промогрупою та діє до {expires_at}.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Індивідуальна знижка {percent}%{tariff_label}\n\nМинуло {trigger_days} днів без підписки. Поверніться та активуйте додаткову знижку — вона сумується з промогрупою та діє до {expires_at}.", "SUBSCRIPTION_EXPIRING": "\n⚠️ Підписка закінчується!\n\nВаша підписка закінчується через {days} днів.\n\nНе забудьте продовжити підписку, щоб не втратити доступ до серверів.\n", - "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ Підписка закінчується через {days_text}!\n\nВаша платна підписка закінчується {end_date}.\n\n💳 Автоплатіж: {autopay_status}\n\n{action_text}\n", + "SUBSCRIPTION_EXPIRING_PAID": "\n⚠️ Підписка{tariff_label} закінчується через {days_text}!\n\nВаша платна підписка закінчується {end_date}.\n\n💳 Автоплатіж: {autopay_status}\n\n{action_text}\n", "SUBSCRIPTION_EXTEND": "💎 Продовжити підписку", "SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "
{crypto_link}
", "SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 Посилання на підписку створено. Натисніть кнопку \"Підключитися\" нижче, щоб відкрити її в Happ.", @@ -1398,12 +1398,14 @@ "SUBSCRIPTION_NO_SERVERS": "Немає серверів", "SUBSCRIPTION_OVERVIEW_TEMPLATE": "👤 {full_name}\n💰 Баланс: {balance}\n📱 Підписка: {status_emoji} {status_display}{warning}{tariff_info_block}\n\n📱 Інформація про підписку\n🎭 Тип: {subscription_type}\n📅 Діє до: {end_date}\n⏰ Залишилося: {time_left}\n📈 Трафік: {traffic}\n🌍 Сервери: {servers}\n📱 Пристрої: {devices_used} / {device_limit}", "SUBSCRIPTION_DAILY_OVERVIEW_TEMPLATE": "👤 {full_name}\n💰 Баланс: {balance}\n📱 Підписка: {status_emoji} {status_display}{warning}{tariff_info_block}\n\n📱 Інформація про підписку\n🎭 Тип: {subscription_type}\n📈 Трафік: {traffic}\n🌍 Сервери: {servers}\n📱 Пристрої: {devices_used} / {device_limit}", - "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "Вашу підписку призупинено, оскільки ви відписались від обов'язкового каналу.\n\nПідпишіться на всі канали для відновлення доступу до VPN.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "🚫 Вашу підписку призупинено, оскільки ви відписались від обов'язкового каналу.\n\nПідпишіться на всі канали для відновлення доступу до VPN.", + "SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI": "🚫 Ваші підписки призупинено, оскільки ви відписались від обов'язкового каналу.\n\nПідпишіться на всі канали для відновлення доступу до VPN.", "SUBSCRIPTION_PROMO_DISCOUNT_HINT": "⚡ Активовано дод. знижку {percent}%. \n\nСумується з іншими знижками!", "SUBSCRIPTION_PROMO_DISCOUNT_NOTE": "⚡ Дод. знижка {percent}%: -{amount}", "SUBSCRIPTION_PROMO_DISCOUNT_TIMER": "⏳ Знижка діє ще: {time_left}\n{bar}", "SUBSCRIPTION_PURCHASED": "🎉 Підписку успішно придбано!", - "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "Вашу підписку відновлено!\n\nДякуємо за підписку на канали. VPN знову активний.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "✅ Вашу підписку відновлено!\n\nДякуємо за підписку на канали. VPN знову активний.", + "SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE_MULTI": "✅ Ваші підписки відновлено!\n\nДякуємо за підписку на канали. VPN знову активний.", "SUBSCRIPTION_SETTINGS_BUTTON": "⚙️ Налаштування підписки", "SUBSCRIPTION_SETTINGS_OVERVIEW": "⚙️ Налаштування підписки\n\n📊 Поточні параметри:\n🌐 Країн: {countries_count}\n📈 Трафік: {traffic_used} / {traffic_limit}\n📱 Пристрої: {devices_used} / {devices_limit}\n\nОберіть що хочете змінити:", "SUBSCRIPTION_SETTINGS_PAID_ONLY": "⚠️ Налаштування доступні лише для платних підписок", @@ -1628,22 +1630,22 @@ "DAILY_SUBSCRIPTION_PAUSED": "⏸️ Підписка призупинена", "DAILY_SUBSCRIPTION_RESUMED": "▶️ Підписка відновлена!", "DAILY_SUBSCRIPTION_RESUMED_AFTER_TOPUP": "✅ Підписка відновлена!\n\nВаш добовий тариф «{tariff_name}» відновлено після поповнення балансу.\n\n💳 Списано: {amount}\n💰 Залишок: {balance}", - "WEBHOOK_SUB_EXPIRED": "❌ Підписка закінчилась\n\nВаша підписка завершена. Продовжте підписку, щоб відновити доступ до VPN.", - "WEBHOOK_SUB_DISABLED": "🚫 Підписку вимкнено\n\nВашу підписку було вимкнено адміністратором.", - "WEBHOOK_SUB_ENABLED": "✅ Підписку активовано\n\nВаша підписка знову активна. Приємного використання!", - "WEBHOOK_SUB_LIMITED": "⚠️ Досягнуто ліміт трафіку\n\nВи вичерпали весь доступний трафік. Докупіть трафік або дочекайтесь скидання.", - "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 Трафік скинуто\n\nВаш трафік обнулено. Ліміт знову доступний у повному обсязі.", - "WEBHOOK_SUB_DELETED": "🗑️ Підписку видалено\n\nВашу підписку було видалено з панелі.", - "WEBHOOK_SUB_REVOKED": "🔑 Ключ підписки оновлено\n\nВаші дані підключення були оновлені. Використовуйте нове посилання для підключення.", - "WEBHOOK_SUB_EXPIRES_72H": "⏳ Підписка закінчується через 3 дні\n\nНе забудьте продовжити підписку, щоб не втратити доступ.", - "WEBHOOK_SUB_EXPIRES_48H": "⏳ Підписка закінчується через 2 дні\n\nПродовжте підписку заздалегідь, щоб зберегти доступ до VPN.", - "WEBHOOK_SUB_EXPIRES_24H": "🔴 Підписка закінчується завтра!\n\nЗалишилось менше 24 годин. Продовжте підписку прямо зараз.", - "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 Підписка закінчилась вчора\n\nВаша підписка завершена. Продовжте зараз, щоб повернути доступ до VPN.", - "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 Перше підключення!\n\nВи вперше підключились до VPN. Ласкаво просимо!", - "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 Використано {percent}% трафіку\n\nВи використали значну частину трафіку. Слідкуйте за витратою.", + "WEBHOOK_SUB_EXPIRED": "❌ Підписка{tariff_label} закінчилась\n\nВаша підписка завершена. Продовжте підписку, щоб відновити доступ до VPN.", + "WEBHOOK_SUB_DISABLED": "🚫 Підписку{tariff_label} вимкнено\n\nВашу підписку було вимкнено адміністратором.", + "WEBHOOK_SUB_ENABLED": "✅ Підписку{tariff_label} активовано\n\nВаша підписка знову активна. Приємного використання!", + "WEBHOOK_SUB_LIMITED": "⚠️ Досягнуто ліміт трафіку\n\nВи вичерпали весь доступний трафік по підписці{tariff_label}. Докупіть трафік або дочекайтесь скидання.", + "WEBHOOK_SUB_TRAFFIC_RESET": "🔄 Трафік скинуто\n\nТрафік по підписці{tariff_label} обнулено. Ліміт знову доступний у повному обсязі.", + "WEBHOOK_SUB_DELETED": "🗑️ Підписку{tariff_label} видалено\n\nВашу підписку було видалено з панелі.", + "WEBHOOK_SUB_REVOKED": "🔑 Ключ підписки{tariff_label} оновлено\n\nВаші дані підключення були оновлені. Використовуйте нове посилання для підключення.", + "WEBHOOK_SUB_EXPIRES_72H": "⏳ Підписка{tariff_label} закінчується через 3 дні\n\nНе забудьте продовжити підписку, щоб не втратити доступ.", + "WEBHOOK_SUB_EXPIRES_48H": "⏳ Підписка{tariff_label} закінчується через 2 дні\n\nПродовжте підписку заздалегідь, щоб зберегти доступ до VPN.", + "WEBHOOK_SUB_EXPIRES_24H": "🔴 Підписка{tariff_label} закінчується завтра!\n\nЗалишилось менше 24 годин. Продовжте підписку прямо зараз.", + "WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 Підписка{tariff_label} закінчилась вчора\n\nВаша підписка завершена. Продовжте зараз, щоб повернути доступ до VPN.", + "WEBHOOK_SUB_FIRST_CONNECTED": "🎉 Перше підключення!\n\nВи вперше підключились до VPN{tariff_label}. Ласкаво просимо!", + "WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 Використано {percent}% трафіку\n\nВи використали значну частину трафіку по підписці{tariff_label}. Слідкуйте за витратою.", "WEBHOOK_RENEW_BUTTON": "🔄 Продовжити підписку", - "WEBHOOK_USER_NOT_CONNECTED": "📡 Ви ще не підключились\n\nВаша підписка активна, але VPN-з'єднання не було встановлено. Підключіться, щоб почати користуватися.", - "WEBHOOK_DEVICE_ADDED": "📱 Новий пристрій\n\nДо вашої підписки підключено новий пристрій: {device}", - "WEBHOOK_DEVICE_DELETED": "📱 Пристрій видалено\n\nПристрій відключено від підписки: {device}", + "WEBHOOK_USER_NOT_CONNECTED": "📡 Ви ще не підключились\n\nВаша підписка{tariff_label} активна, але VPN-з'єднання не було встановлено. Підключіться, щоб почати користуватися.", + "WEBHOOK_DEVICE_ADDED": "📱 Новий пристрій\n\nДо підписки{tariff_label} підключено новий пристрій: {device}", + "WEBHOOK_DEVICE_DELETED": "📱 Пристрій видалено\n\nПристрій відключено від підписки{tariff_label}: {device}", "WEBHOOK_CLOSE_BUTTON": "✖️ Закрити" } diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json index 936bd0a3..4af099bc 100644 --- a/app/localization/locales/zh.json +++ b/app/localization/locales/zh.json @@ -1364,11 +1364,11 @@ "SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "步骤3-连接:", "SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "步骤1-安装:", "SUBSCRIPTION_EXPIRED": "\n❌订阅已过期\n\n您的订阅已过期。要恢复访问权限,请续订。\n", -"SUBSCRIPTION_EXPIRED_1D": "⛔订阅已结束\n\n访问权限已于{end_date}禁用。请续订以恢复完全访问。\n\n💎续订费用:{price}", -"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥续订{percent}%折扣\n\n激活优惠以获得额外折扣。此折扣可与您的促销组叠加,有效期至{expires_at}。", -"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁个人{percent}%折扣\n\n您已{trigger_days}天没有订阅。回来激活额外折扣—此折扣可与促销组叠加,有效期至{expires_at}。", +"SUBSCRIPTION_EXPIRED_1D": "⛔订阅{tariff_label}已结束\n\n访问权限已于{end_date}禁用。请续订以恢复完全访问。\n\n💎续订费用:{price}", +"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥续订{tariff_label}{percent}%折扣\n\n激活优惠以获得额外折扣。此折扣可与您的促销组叠加,有效期至{expires_at}。", +"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁个人{percent}%折扣{tariff_label}\n\n您已{trigger_days}天没有订阅。回来激活额外折扣—此折扣可与促销组叠加,有效期至{expires_at}。", "SUBSCRIPTION_EXPIRING": "\n⚠️订阅即将过期!\n\n您的订阅将在{days}天后过期。\n\n别忘记续订,以免失去服务器访问权限。\n", -"SUBSCRIPTION_EXPIRING_PAID": "\n⚠️订阅将在{days_text}后过期!\n\n您的付费订阅将于{end_date}过期。\n\n💳自动支付:{autopay_status}\n\n{action_text}\n", +"SUBSCRIPTION_EXPIRING_PAID": "\n⚠️订阅{tariff_label}将在{days_text}后过期!\n\n您的付费订阅将于{end_date}过期。\n\n💳自动支付:{autopay_status}\n\n{action_text}\n", "SUBSCRIPTION_EXTEND": "💎延长订阅", "SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "{crypto_link}", "SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒订阅链接已创建。点击下方的“连接”按钮以在Happ中打开。", @@ -1394,12 +1394,14 @@ "SUBSCRIPTION_NO_SERVERS": "没有服务器", "SUBSCRIPTION_OVERVIEW_TEMPLATE": "👤{full_name}\n💰余额:{balance}\n📱订阅:{status_emoji}{status_display}{warning}{tariff_info_block}\n\n📱订阅信息\n🎭类型:{subscription_type}\n📅有效期至:{end_date}\n⏰剩余时间:{time_left}\n📈流量:{traffic}\n🌍服务器:{servers}\n📱设备:{devices_used}/{device_limit}", "SUBSCRIPTION_DAILY_OVERVIEW_TEMPLATE": "👤{full_name}\n💰余额:{balance}\n📱订阅:{status_emoji}{status_display}{warning}{tariff_info_block}\n\n📱订阅信息\n🎭类型:{subscription_type}\n📈流量:{traffic}\n🌍服务器:{servers}\n📱设备:{devices_used}/{device_limit}", -"SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "由于您退出了必需频道,您的订阅已暂停。\n\n请订阅所有频道以恢复VPN访问。", +"SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE": "🚫 由于您退出了必需频道,您的订阅已暂停。\n\n请订阅所有频道以恢复VPN访问。", +"SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI": "🚫 由于您退出了必需频道,您的所有订阅已暂停。\n\n请订阅所有频道以恢复VPN访问。", "SUBSCRIPTION_PROMO_DISCOUNT_HINT": "⚡已激活额外{percent}%折扣。\n\n可与其他折扣叠加!", "SUBSCRIPTION_PROMO_DISCOUNT_NOTE": "⚡额外{percent}%折扣:-{amount}", "SUBSCRIPTION_PROMO_DISCOUNT_TIMER": "⏳折扣剩余时间:{time_left}\n{bar}", "SUBSCRIPTION_PURCHASED": "🎉订阅已成功购买!", -"SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "您的订阅已恢复!\n\n感谢您订阅频道。VPN已重新激活。", +"SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE": "✅ 您的订阅已恢复!\n\n感谢您订阅频道。VPN已重新激活。", +"SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE_MULTI": "✅ 您的所有订阅已恢复!\n\n感谢您订阅频道。VPN已重新激活。", "SUBSCRIPTION_SETTINGS_BUTTON": "⚙️订阅设置", "SUBSCRIPTION_SETTINGS_OVERVIEW": "⚙️订阅设置\n\n📊当前参数:\n🌐国家:{countries_count}\n📈流量:{traffic_used}/{traffic_limit}\n📱设备:{devices_used}/{devices_limit}\n\n请选择要更改的内容:", "SUBSCRIPTION_SETTINGS_PAID_ONLY": "⚠️设置仅适用于付费订阅", @@ -1624,23 +1626,23 @@ "DAILY_SUBSCRIPTION_PAUSED": "⏸️ 订阅已暂停", "DAILY_SUBSCRIPTION_RESUMED": "▶️ 订阅已恢复!", "DAILY_SUBSCRIPTION_RESUMED_AFTER_TOPUP": "✅ 订阅已恢复!\n\n您的日套餐「{tariff_name}」已在充值后恢复。\n\n💳 扣费:{amount}\n💰 余额:{balance}", -"WEBHOOK_SUB_EXPIRED": "❌ 订阅已过期\n\n您的订阅已结束。请续订以恢复VPN访问。", -"WEBHOOK_SUB_DISABLED": "🚫 订阅已禁用\n\n您的订阅已被管理员禁用。", -"WEBHOOK_SUB_ENABLED": "✅ 订阅已激活\n\n您的订阅已重新激活。祝使用愉快!", -"WEBHOOK_SUB_LIMITED": "⚠️ 已达到流量限制\n\n您已用完所有可用流量。请购买额外流量或等待重置。", -"WEBHOOK_SUB_TRAFFIC_RESET": "🔄 流量已重置\n\n您的流量已重置。完整限额再次可用。", -"WEBHOOK_SUB_DELETED": "🗑️ 订阅已删除\n\n您的订阅已从面板中删除。", -"WEBHOOK_SUB_REVOKED": "🔑 订阅密钥已更新\n\n您的连接凭证已更新。请使用新链接进行连接。", -"WEBHOOK_SUB_EXPIRES_72H": "⏳ 订阅将在3天后到期\n\n请记得续订以保持VPN访问。", -"WEBHOOK_SUB_EXPIRES_48H": "⏳ 订阅将在2天后到期\n\n请提前续订以保持VPN访问。", -"WEBHOOK_SUB_EXPIRES_24H": "🔴 订阅明天到期!\n\n不到24小时。请立即续订。", -"WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 订阅昨天已过期\n\n您的订阅已结束。请立即续订以恢复VPN访问。", -"WEBHOOK_SUB_FIRST_CONNECTED": "🎉 首次连接!\n\n您首次连接了VPN。欢迎!", -"WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 已使用{percent}%流量\n\n您已使用了大部分流量。请注意使用量。", +"WEBHOOK_SUB_EXPIRED": "❌ 订阅{tariff_label}已过期\n\n您的订阅已结束。请续订以恢复VPN访问。", +"WEBHOOK_SUB_DISABLED": "🚫 订阅{tariff_label}已被禁用\n\n您的订阅已被管理员禁用。", +"WEBHOOK_SUB_ENABLED": "✅ 订阅{tariff_label}已激活\n\n您的订阅已重新激活。祝使用愉快!", +"WEBHOOK_SUB_LIMITED": "⚠️ 已达到流量限制\n\n您已用完订阅{tariff_label}的所有可用流量。请购买额外流量或等待重置。", +"WEBHOOK_SUB_TRAFFIC_RESET": "🔄 流量已重置\n\n订阅{tariff_label}的流量已重置。完整限额再次可用。", +"WEBHOOK_SUB_DELETED": "🗑️ 订阅{tariff_label}已删除\n\n您的订阅已从面板中删除。", +"WEBHOOK_SUB_REVOKED": "🔑 订阅{tariff_label}密钥已更新\n\n您的连接凭证已更新。请使用新链接进行连接。", +"WEBHOOK_SUB_EXPIRES_72H": "⏳ 订阅{tariff_label}将在3天后到期\n\n请记得续订以保持VPN访问。", +"WEBHOOK_SUB_EXPIRES_48H": "⏳ 订阅{tariff_label}将在2天后到期\n\n请提前续订以保持VPN访问。", +"WEBHOOK_SUB_EXPIRES_24H": "🔴 订阅{tariff_label}明天到期!\n\n不到24小时。请立即续订。", +"WEBHOOK_SUB_EXPIRED_24H_AGO": "💤 订阅{tariff_label}昨天已过期\n\n您的订阅已结束。请立即续订以恢复VPN访问。", +"WEBHOOK_SUB_FIRST_CONNECTED": "🎉 首次连接!\n\n您首次连接了VPN{tariff_label}。欢迎!", +"WEBHOOK_SUB_BANDWIDTH_THRESHOLD": "📊 已使用{percent}%流量\n\n您已使用了订阅{tariff_label}的大部分流量。请注意使用量。", "WEBHOOK_RENEW_BUTTON": "🔄 续订订阅", -"WEBHOOK_USER_NOT_CONNECTED": "📡 尚未连接\n\n您的订阅已激活,但尚未建立VPN连接。请连接以开始使用服务。", -"WEBHOOK_DEVICE_ADDED": "📱 新设备\n\n您的订阅已添加新设备:{device}", -"WEBHOOK_DEVICE_DELETED": "📱 设备已移除\n\n设备已从您的订阅中移除:{device}", +"WEBHOOK_USER_NOT_CONNECTED": "📡 尚未连接\n\n您的订阅{tariff_label}已激活,但尚未建立VPN连接。请连接以开始使用服务。", +"WEBHOOK_DEVICE_ADDED": "📱 新设备\n\n订阅{tariff_label}已添加新设备:{device}", +"WEBHOOK_DEVICE_DELETED": "📱 设备已移除\n\n设备已从订阅{tariff_label}中移除:{device}", "WEBHOOK_CLOSE_BUTTON": "✖️ 关闭", "BALANCE_TOPPED_UP_CART_SUFFICIENT": "✅ 余额已充值 {amount}!\n\n💰 当前余额:{balance}\n\n🛒 您有一个已保存的购物车,金额为 {cart_total}\n余额足够完成订购。", "BALANCE_TOPPED_UP_CART_INSUFFICIENT": "✅ 余额已充值 {amount}!\n\n💰 当前余额:{balance}\n\n🛒 您有一个已保存的购物车,金额为 {cart_total}\n还差:{missing}" diff --git a/app/middlewares/channel_checker.py b/app/middlewares/channel_checker.py index 18fbcc4b..fd164c5d 100644 --- a/app/middlewares/channel_checker.py +++ b/app/middlewares/channel_checker.py @@ -407,11 +407,18 @@ class ChannelCheckerMiddleware(BaseMiddleware): try: normalized = _normalize_channels(channels) texts = get_texts(user.language or DEFAULT_LANGUAGE) - notification_text = texts.t( - 'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE', - '🚫 Ваша подписка приостановлена, так как вы отписались от канала.\n\n' - 'Подпишитесь на канал снова, чтобы восстановить доступ к VPN.', - ) + if settings.is_multi_tariff_enabled() and len(active_subs) > 1: + notification_text = texts.t( + 'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI', + '🚫 Ваши подписки приостановлены, так как вы отписались от обязательного канала.\n\n' + 'Подпишитесь на все каналы для восстановления доступа к VPN.', + ) + else: + notification_text = texts.t( + 'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE', + '🚫 Ваша подписка приостановлена, так как вы отписались от канала.\n\n' + 'Подпишитесь на канал снова, чтобы восстановить доступ к VPN.', + ) channel_kb = get_channel_sub_keyboard(normalized, language=user.language) await bot.send_message(telegram_id, notification_text, reply_markup=channel_kb) except Exception as notify_error: @@ -484,10 +491,16 @@ class ChannelCheckerMiddleware(BaseMiddleware): # Notify user about reactivation try: texts = get_texts(user.language or DEFAULT_LANGUAGE) - notification_text = texts.t( - 'SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE', - '✅ Ваша подписка восстановлена!\n\nСпасибо, что подписались на канал. VPN снова работает.', - ) + if settings.is_multi_tariff_enabled() and len(disabled_subs) > 1: + notification_text = texts.t( + 'SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE_MULTI', + '✅ Ваши подписки восстановлены!\n\nСпасибо, что подписались на канал. VPN снова работает.', + ) + else: + notification_text = texts.t( + 'SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE', + '✅ Ваша подписка восстановлена!\n\nСпасибо, что подписались на канал. VPN снова работает.', + ) await bot.send_message(telegram_id, notification_text) except Exception as notify_error: logger.warning( diff --git a/app/services/daily_subscription_service.py b/app/services/daily_subscription_service.py index cfccf8fe..4ac850e8 100644 --- a/app/services/daily_subscription_service.py +++ b/app/services/daily_subscription_service.py @@ -333,8 +333,11 @@ class DailySubscriptionService: required_rubles = required_amount / 100 balance_rubles = user.balance_kopeks / 100 + tariff_label = '' + if settings.is_multi_tariff_enabled() and hasattr(subscription, 'tariff') and subscription.tariff: + tariff_label = f' «{subscription.tariff.name}»' message = ( - f'⚠️ Подписка приостановлена\n\n' + f'⚠️ Подписка{tariff_label} приостановлена\n\n' f'Недостаточно средств для суточной оплаты.\n\n' f'Требуется: {required_rubles:.2f} ₽\n' f'Баланс: {balance_rubles:.2f} ₽\n\n' diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index c3c52b61..31aa03eb 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -294,11 +294,14 @@ class MonitoringService: from app.database.crud.subscription import expire_subscription + # Capture tariff name before expire_subscription's db.refresh() expires the relationship + _tariff_name = subscription.tariff.name if getattr(subscription, 'tariff', None) else None + await expire_subscription(db, subscription) user = await get_user_by_id(db, subscription.user_id) if user and self.bot: - await self._send_subscription_expired_notification(user, subscription) + await self._send_subscription_expired_notification(user, subscription, tariff_name=_tariff_name) logger.info( "🔴 Подписка пользователя истекла и статус изменен на 'expired'", user_id=subscription.user_id @@ -539,6 +542,7 @@ class MonitoringService: select(Subscription) .join(Subscription.user) .options( + selectinload(Subscription.tariff), selectinload(Subscription.user).selectinload(User.promo_group), selectinload(Subscription.user) .selectinload(User.user_promo_groups) @@ -1233,7 +1237,9 @@ class MonitoringService: else: failed_count += 1 if user.telegram_id and self.bot: - await self._send_autopay_failed_notification(user, user.balance_kopeks, charge_amount) + await self._send_autopay_failed_notification( + user, user.balance_kopeks, charge_amount, subscription=subscription + ) elif not user.telegram_id: await notification_delivery_service.notify_autopay_failed( user=user, @@ -1267,7 +1273,9 @@ class MonitoringService: if should_notify: if user.telegram_id and self.bot: - await self._send_autopay_failed_notification(user, user.balance_kopeks, charge_amount) + await self._send_autopay_failed_notification( + user, user.balance_kopeks, charge_amount, subscription=subscription + ) elif not user.telegram_id: await notification_delivery_service.notify_autopay_failed( user=user, @@ -1303,10 +1311,18 @@ class MonitoringService: except Exception as e: logger.error('Ошибка обработки автоплатежей', error=e) - async def _send_subscription_expired_notification(self, user: User, subscription: Subscription) -> bool: + async def _send_subscription_expired_notification( + self, user: User, subscription: Subscription, *, tariff_name: str | None = None + ) -> bool: try: - message = """ -⛔ Подписка истекла + tariff_label = '' + if settings.is_multi_tariff_enabled(): + if tariff_name: + tariff_label = f' «{tariff_name}»' + elif hasattr(subscription, 'tariff') and subscription.tariff: + tariff_label = f' «{subscription.tariff.name}»' + message = f""" +⛔ Подписка{tariff_label} истекла Ваша подписка истекла. Для восстановления доступа продлите подписку. @@ -1466,8 +1482,11 @@ class MonitoringService: try: get_texts(user.language) - message = """ -🎁 Тестовая подписка скоро закончится! + tariff_label = '' + if settings.is_multi_tariff_enabled() and hasattr(subscription, 'tariff') and subscription.tariff: + tariff_label = f' «{subscription.tariff.name}»' + message = f""" +🎁 Тестовая подписка{tariff_label} скоро закончится! Ваша тестовая подписка истекает через 2 часа. @@ -1593,16 +1612,20 @@ class MonitoringService: async def _send_expired_day1_notification(self, user: User, subscription: Subscription) -> bool: try: texts = get_texts(user.language) + tariff_label = '' + if settings.is_multi_tariff_enabled() and hasattr(subscription, 'tariff') and subscription.tariff: + tariff_label = f' «{subscription.tariff.name}»' template = texts.get( 'SUBSCRIPTION_EXPIRED_1D', ( - '⛔ Подписка закончилась\n\n' + '⛔ Подписка{tariff_label} закончилась\n\n' 'Доступ был отключён {end_date}. Продлите подписку, чтобы вернуться в сервис.' ), ) message = template.format( end_date=format_local_datetime(subscription.end_date, '%d.%m.%Y %H:%M'), price=settings.format_price(settings.PRICE_30_DAYS), + tariff_label=tariff_label, ) from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup @@ -1672,11 +1695,15 @@ class MonitoringService: try: texts = get_texts(user.language) + tariff_label = '' + if settings.is_multi_tariff_enabled() and hasattr(subscription, 'tariff') and subscription.tariff: + tariff_label = f' «{subscription.tariff.name}»' + if wave == 'second': template = texts.get( 'SUBSCRIPTION_EXPIRED_SECOND_WAVE', ( - '🔥 Скидка {percent}% на продление\n\n' + '🔥 Скидка {percent}% на продление{tariff_label}\n\n' 'Активируйте предложение, чтобы получить дополнительную скидку. ' 'Она суммируется с вашей промогруппой и действует до {expires_at}.' ), @@ -1685,7 +1712,7 @@ class MonitoringService: template = texts.get( 'SUBSCRIPTION_EXPIRED_THIRD_WAVE', ( - '🎁 Индивидуальная скидка {percent}%\n\n' + '🎁 Индивидуальная скидка {percent}%{tariff_label}\n\n' 'Прошло {trigger_days} дней без подписки — возвращайтесь и активируйте дополнительную скидку. ' 'Она суммируется с промогруппой и действует до {expires_at}.' ), @@ -1695,6 +1722,7 @@ class MonitoringService: percent=percent, expires_at=format_local_datetime(expires_at, '%d.%m.%Y %H:%M'), trigger_days=trigger_days or '', + tariff_label=tariff_label, ) from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup @@ -1767,7 +1795,7 @@ class MonitoringService: tariff_label = f' «{subscription.tariff.name}»' message = texts.AUTOPAY_SUCCESS.format(days=days, amount=settings.format_price(amount)) if tariff_label: - message += f'\n📦 Тариф:{tariff_label}' + message += f'\n📦 Тариф: {tariff_label}' await self._send_message_with_logo( chat_id=user.telegram_id, text=message, @@ -1787,12 +1815,21 @@ class MonitoringService: except Exception as e: logger.error('Ошибка отправки уведомления об автоплатеже пользователю', telegram_id=user.telegram_id, e=e) - async def _send_autopay_failed_notification(self, user: User, balance: int, required: int): + async def _send_autopay_failed_notification( + self, user: User, balance: int, required: int, *, subscription: Subscription | None = None + ): try: texts = get_texts(user.language) message = texts.AUTOPAY_FAILED.format( balance=settings.format_price(balance), required=settings.format_price(required) ) + if ( + settings.is_multi_tariff_enabled() + and subscription + and hasattr(subscription, 'tariff') + and subscription.tariff + ): + message += f'\n📦 Тариф: «{subscription.tariff.name}»' from aiogram.types import InlineKeyboardMarkup diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py index d045bf1a..b21bf42e 100644 --- a/app/services/payment/stars.py +++ b/app/services/payment/stars.py @@ -304,12 +304,23 @@ class TelegramStarsMixin: traffic_limit = getattr(subscription, 'traffic_limit_gb', 0) or 0 traffic_label = 'Безлимит' if traffic_limit == 0 else f'{int(traffic_limit)} ГБ' + tariff_line = '' + if settings.is_multi_tariff_enabled() and getattr(subscription, 'tariff_id', None): + try: + from app.database.crud.tariff import get_tariff_by_id + + _t = await get_tariff_by_id(db, subscription.tariff_id) + if _t: + tariff_line = f'\n📦 Тариф: «{_t.name}»' + except Exception: + pass success_message = ( '✅ Подписка успешно активирована!\n\n' f'📅 Период: {period_display} дней\n' f'📱 Устройства: {getattr(subscription, "device_limit", 1)}\n' f'📊 Трафик: {traffic_label}\n' - f'⭐ Оплата: {stars_amount} ⭐ ({settings.format_price(amount_kopeks)})\n\n' + f'⭐ Оплата: {stars_amount} ⭐ ({settings.format_price(amount_kopeks)})' + f'{tariff_line}\n\n' "🔗 Для подключения перейдите в раздел 'Моя подписка'" ) diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 43238762..0ce2f94c 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -996,12 +996,23 @@ class YooKassaPaymentMixin: if getattr(self, 'bot', None) and user.telegram_id: from aiogram import types + tariff_line = '' + if settings.is_multi_tariff_enabled() and getattr(subscription, 'tariff_id', None): + try: + from app.database.crud.tariff import get_tariff_by_id + + _t = await get_tariff_by_id(db, subscription.tariff_id) + if _t: + tariff_line = f'\n📦 Тариф: «{_t.name}»' + except Exception: + pass success_message = ( f'✅ Подписка успешно активирована!\n\n' f'📅 Период: {subscription_period} дней\n' f'📱 Устройства: 1\n' f'📊 Трафик: Безлимит\n' - f'💳 Оплата: {settings.format_price(payment.amount_kopeks)} (YooKassa)\n\n' + f'💳 Оплата: {settings.format_price(payment.amount_kopeks)} (YooKassa)' + f'{tariff_line}\n\n' f"🔗 Для подключения перейдите в раздел 'Моя подписка'" ) diff --git a/app/services/recurrent_payment_service.py b/app/services/recurrent_payment_service.py index 2b8739f7..58a7c5eb 100644 --- a/app/services/recurrent_payment_service.py +++ b/app/services/recurrent_payment_service.py @@ -363,6 +363,8 @@ async def _process_single_subscription( 'RECURRENT_TOPUP_SUCCESS', '✅ Автоплатёж выполнен\n\nБаланс пополнен на {amount} для продления подписки.', ).format(amount=settings.format_price(topup_amount_kopeks)) + if settings.is_multi_tariff_enabled() and hasattr(subscription, 'tariff') and subscription.tariff: + msg += f'\n📦 Тариф: «{subscription.tariff.name}»' await bot.send_message( chat_id=user.telegram_id, text=msg, @@ -391,6 +393,8 @@ async def _process_single_subscription( 'RECURRENT_TOPUP_FAILED', '❌ Автоплатёж не удался\n\nНе удалось списать {amount} ни с одной сохранённой карты для продления подписки.\n\nПополните баланс вручную, чтобы подписка не прервалась.', ).format(amount=settings.format_price(topup_amount_kopeks)) + if settings.is_multi_tariff_enabled() and hasattr(subscription, 'tariff') and subscription.tariff: + msg += f'\n📦 Тариф: «{subscription.tariff.name}»' await bot.send_message( chat_id=user.telegram_id, text=msg, diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 40c667f9..50209afe 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -552,6 +552,7 @@ class RemnaWaveWebhookService: *, reply_markup: InlineKeyboardMarkup | None = None, format_kwargs: dict[str, Any] | None = None, + subscription: Subscription | None = None, ) -> None: """Send a notification to user via appropriate channel. @@ -577,6 +578,15 @@ class RemnaWaveWebhookService: logger.warning('Missing locale key for language', text_key=text_key, language=user.language) return + # Inject tariff_label for multi-tariff subscription identification + if format_kwargs is None: + format_kwargs = {} + if 'tariff_label' not in format_kwargs: + tariff_label = '' + if settings.is_multi_tariff_enabled() and subscription and getattr(subscription, 'tariff', None): + tariff_label = f' «{subscription.tariff.name}»' + format_kwargs['tariff_label'] = tariff_label + if format_kwargs: try: message = message.format(**format_kwargs) @@ -664,6 +674,7 @@ class RemnaWaveWebhookService: user, 'WEBHOOK_SUB_EXPIRED', reply_markup=self._get_renew_keyboard(user, subscription.id), + subscription=subscription, ) async def _handle_user_disabled( @@ -698,7 +709,9 @@ class RemnaWaveWebhookService: else: await db.commit() - await self._notify_user(user, 'WEBHOOK_SUB_DISABLED', reply_markup=self._get_subscription_keyboard(user)) + await self._notify_user( + user, 'WEBHOOK_SUB_DISABLED', reply_markup=self._get_subscription_keyboard(user), subscription=subscription + ) async def _handle_user_enabled( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict @@ -714,7 +727,9 @@ class RemnaWaveWebhookService: else: await db.commit() - await self._notify_user(user, 'WEBHOOK_SUB_ENABLED', reply_markup=self._get_connect_keyboard(user)) + await self._notify_user( + user, 'WEBHOOK_SUB_ENABLED', reply_markup=self._get_connect_keyboard(user), subscription=subscription + ) async def _handle_user_limited( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict @@ -735,7 +750,9 @@ class RemnaWaveWebhookService: else: await db.commit() - await self._notify_user(user, 'WEBHOOK_SUB_LIMITED', reply_markup=self._get_traffic_keyboard(user)) + await self._notify_user( + user, 'WEBHOOK_SUB_LIMITED', reply_markup=self._get_traffic_keyboard(user), subscription=subscription + ) async def _handle_user_traffic_reset( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict @@ -751,7 +768,12 @@ class RemnaWaveWebhookService: await reactivate_subscription(db, subscription) logger.info('Webhook: traffic reset for subscription , user', subscription_id=subscription.id, user_id=user.id) - await self._notify_user(user, 'WEBHOOK_SUB_TRAFFIC_RESET', reply_markup=self._get_subscription_keyboard(user)) + await self._notify_user( + user, + 'WEBHOOK_SUB_TRAFFIC_RESET', + reply_markup=self._get_subscription_keyboard(user), + subscription=subscription, + ) async def _handle_user_modified( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict @@ -1003,6 +1025,7 @@ class RemnaWaveWebhookService: reply_markup=self._get_renew_keyboard( user, getattr(subscription, 'id', None) if subscription else None ), + subscription=subscription, ) else: await self._notify_user( @@ -1011,6 +1034,7 @@ class RemnaWaveWebhookService: reply_markup=self._get_renew_keyboard( user, getattr(subscription, 'id', None) if subscription else None ), + subscription=subscription, ) async def _attempt_panel_recreation(self, db: AsyncSession, user: User, subscription: Subscription) -> bool: @@ -1096,7 +1120,9 @@ class RemnaWaveWebhookService: ) await db.commit() - await self._notify_user(user, 'WEBHOOK_SUB_REVOKED', reply_markup=self._get_connect_keyboard(user)) + await self._notify_user( + user, 'WEBHOOK_SUB_REVOKED', reply_markup=self._get_connect_keyboard(user), subscription=subscription + ) async def _handle_user_created( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict @@ -1110,7 +1136,10 @@ class RemnaWaveWebhookService: logger.info('Webhook expires_72h: подписка не найдена в БД, пропуск', user_id=user.id) return await self._notify_user( - user, 'WEBHOOK_SUB_EXPIRES_72H', reply_markup=self._get_renew_keyboard(user, subscription.id) + user, + 'WEBHOOK_SUB_EXPIRES_72H', + reply_markup=self._get_renew_keyboard(user, subscription.id), + subscription=subscription, ) async def _handle_expires_in_48h( @@ -1120,7 +1149,10 @@ class RemnaWaveWebhookService: logger.info('Webhook expires_48h: подписка не найдена в БД, пропуск', user_id=user.id) return await self._notify_user( - user, 'WEBHOOK_SUB_EXPIRES_48H', reply_markup=self._get_renew_keyboard(user, subscription.id) + user, + 'WEBHOOK_SUB_EXPIRES_48H', + reply_markup=self._get_renew_keyboard(user, subscription.id), + subscription=subscription, ) async def _handle_expires_in_24h( @@ -1130,7 +1162,10 @@ class RemnaWaveWebhookService: logger.info('Webhook expires_24h: подписка не найдена в БД, пропуск', user_id=user.id) return await self._notify_user( - user, 'WEBHOOK_SUB_EXPIRES_24H', reply_markup=self._get_renew_keyboard(user, subscription.id) + user, + 'WEBHOOK_SUB_EXPIRES_24H', + reply_markup=self._get_renew_keyboard(user, subscription.id), + subscription=subscription, ) async def _handle_expired_24h_ago( @@ -1140,14 +1175,22 @@ class RemnaWaveWebhookService: logger.info('Webhook expired_24h_ago: подписка не найдена в БД, пропуск', user_id=user.id) return await self._notify_user( - user, 'WEBHOOK_SUB_EXPIRED_24H_AGO', reply_markup=self._get_renew_keyboard(user, subscription.id) + user, + 'WEBHOOK_SUB_EXPIRED_24H_AGO', + reply_markup=self._get_renew_keyboard(user, subscription.id), + subscription=subscription, ) async def _handle_first_connected( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict ) -> None: logger.info('Webhook: user first VPN connection', user_id=user.id) - await self._notify_user(user, 'WEBHOOK_SUB_FIRST_CONNECTED', reply_markup=self._get_subscription_keyboard(user)) + await self._notify_user( + user, + 'WEBHOOK_SUB_FIRST_CONNECTED', + reply_markup=self._get_subscription_keyboard(user), + subscription=subscription, + ) async def _handle_bandwidth_threshold( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict @@ -1168,6 +1211,7 @@ class RemnaWaveWebhookService: 'WEBHOOK_SUB_BANDWIDTH_THRESHOLD', reply_markup=self._get_traffic_keyboard(user), format_kwargs={'percent': percent_str}, + subscription=subscription, ) async def _handle_user_not_connected( @@ -1188,6 +1232,7 @@ class RemnaWaveWebhookService: 'WEBHOOK_USER_NOT_CONNECTED', reply_markup=self._get_connect_keyboard(user), format_kwargs=format_kwargs if format_kwargs else None, + subscription=subscription, ) # ------------------------------------------------------------------ @@ -1236,6 +1281,7 @@ class RemnaWaveWebhookService: 'WEBHOOK_DEVICE_ADDED', reply_markup=self._get_subscription_keyboard(user), format_kwargs={'device': device_name or '—'}, + subscription=subscription, ) async def _handle_device_deleted( @@ -1248,4 +1294,5 @@ class RemnaWaveWebhookService: 'WEBHOOK_DEVICE_DELETED', reply_markup=self._get_subscription_keyboard(user), format_kwargs={'device': device_name or '—'}, + subscription=subscription, ) diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py index e67db1ea..9380c07e 100644 --- a/app/services/subscription_auto_purchase_service.py +++ b/app/services/subscription_auto_purchase_service.py @@ -66,6 +66,7 @@ class AutoExtendContext: squad_uuid: str | None = None consume_promo_offer: bool = False tariff_id: int | None = None + tariff_name: str | None = None allowed_squads: list | None = None @@ -355,6 +356,7 @@ async def _prepare_auto_extend_context( squad_uuid=squad_uuid, consume_promo_offer=consume_promo_offer, tariff_id=tariff_id, + tariff_name=tariff_name if tariff_id else None, allowed_squads=allowed_squads, ) @@ -623,6 +625,8 @@ async def _auto_extend_subscription( 'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED', '✅ Subscription automatically extended for {period}.', ).format(period=period_label) + if settings.is_multi_tariff_enabled() and prepared.tariff_name: + auto_message += f'\n📦 Тариф: «{prepared.tariff_name}»' details_message = texts.t( 'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED_DETAILS', 'New expiration date: {date}.', @@ -727,6 +731,8 @@ async def _auto_purchase_tariff( return False tariff = await get_tariff_by_id(db, tariff_id) + # Capture name before any db.commit() can expire the ORM object + tariff_name_for_label = tariff.name if tariff else None if not tariff or not tariff.is_active: logger.warning( '🔁 Автопокупка тарифа: тариф недоступен для пользователя', @@ -984,6 +990,8 @@ async def _auto_purchase_tariff( 'AUTO_PURCHASE_SUBSCRIPTION_SUCCESS', '✅ Подписка на {period} автоматически оформлена после пополнения баланса.', ).format(period=period_label) + if settings.is_multi_tariff_enabled() and tariff_name_for_label: + message += f'\n📦 Тариф: «{tariff_name_for_label}»' hint = texts.t( 'AUTO_PURCHASE_SUBSCRIPTION_HINT', @@ -2124,6 +2132,8 @@ async def try_auto_extend_expired_after_topup( # Determine renewal period from tariff or default to 30 days tariff = getattr(subscription, 'tariff', None) + # Capture name before any db.commit() can expire the ORM object + tariff_name_for_label = tariff.name if tariff else None if tariff: period_days = tariff.get_shortest_period() or 30 else: @@ -2359,6 +2369,8 @@ async def try_auto_extend_expired_after_topup( 'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED', '✅ Subscription automatically extended for {period}.', ).format(period=period_label) + if settings.is_multi_tariff_enabled() and tariff_name_for_label: + auto_message += f'\n📦 Тариф: «{tariff_name_for_label}»' details_message = texts.t( 'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED_DETAILS', 'New expiration date: {date}.', @@ -3111,6 +3123,15 @@ async def _process_legacy_generic_cart( 'AUTO_PURCHASE_SUBSCRIPTION_SUCCESS', '✅ Subscription purchased automatically after balance top-up ({period}).', ).format(period=period_label) + if settings.is_multi_tariff_enabled() and subscription and getattr(subscription, 'tariff_id', None): + try: + from app.database.crud.tariff import get_tariff_by_id as _get_tariff_label + + _t = await _get_tariff_label(db, subscription.tariff_id) + if _t: + auto_message += f'\n📦 Тариф: «{_t.name}»' + except Exception: + pass hint_message = texts.t( 'AUTO_PURCHASE_SUBSCRIPTION_HINT',