refactor: remove smart auto-activation & activation prompt, fix production bugs

Remove AUTO_ACTIVATE_AFTER_TOPUP and SHOW_ACTIVATION_PROMPT_AFTER_TOPUP
features from all payment providers, config, system settings, and tests.
Cart auto-purchase (AUTO_PURCHASE_AFTER_TOPUP) is preserved.

Bug fixes:
- fix KeyError 'months' in devices.py for custom locale overrides
- fix IntegrityError on trial subscription retry (update existing PENDING instead of INSERT)
- fix PendingRollbackError cascade by adding db.rollback() before recovery
- fix TelegramForbiddenError not caught in photo_message.py
- fix "query is too old" spam in required_sub_channel_check
- add missing trial locale keys (TRIAL_PAYMENT_DESCRIPTION, TRIAL_REFUND_DESCRIPTION, TRIAL_ACTIVATION_ERROR)
This commit is contained in:
Fringg
2026-02-09 21:39:53 +03:00
parent 65ba50c2cf
commit a3903a252e
30 changed files with 172 additions and 831 deletions
+1 -7
View File
@@ -838,15 +838,9 @@ CONTESTS_BUTTON_VISIBLE=false
# Реферальные конкурсы (турниры среди рефералов)
REFERRAL_CONTESTS_ENABLED=false
# ===== АВТОАКТИВАЦИЯ ПОСЛЕ ПОПОЛНЕНИЯ =====
# ===== АВТОПОКУПКА ПОСЛЕ ПОПОЛНЕНИЯ =====
# Автоматическая покупка из сохранённой корзины после пополнения баланса
AUTO_PURCHASE_AFTER_TOPUP_ENABLED=false
# Умная автоактивация: система сама решает — продлить или создать подписку
# Работает даже без сохранённой корзины. Выбирает максимальный период <= баланса
AUTO_ACTIVATE_AFTER_TOPUP_ENABLED=false
# Показывать предупреждение об активации подписки после пополнения баланса
# Если true - после пополнения показывает сообщение с кнопками: "Активировать", "Продлить", "Добавить устройства"
SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=false
# ===== КНОПКА АКТИВАЦИИ =====
ACTIVATE_BUTTON_VISIBLE=false
-2
View File
@@ -1342,7 +1342,6 @@ CONTEST_BUTTON_VISIBLE=true
- 🔄 Автоплатёж с настройкой дня списания
- 🎁 Реферальные и промо-бонусы
-**Быстрое пополнение** с кнопками быстрых сумм
- 🔄 **Умная автоактивация** подписки после пополнения баланса
📱 **Управление подписками**
@@ -1529,7 +1528,6 @@ CONTEST_BUTTON_VISIBLE=true
- 🔄 **Миграция сквадов** - массовый перенос пользователей между сквадами
- 🧾 **История операций** - хранение всех транзакций и действий для аудита
- 💸 **Сервис автопроверки транзакций** - автоматическая проверка транзакций в статусе "В ожидании оплаты" за последние 24ч
- 🔄 **Умная автоактивация** - автоматическая активация подписки после пополнения баланса
- 📝 **Ротация логов** - автоматическая очистка и архивация старых логов
- 🎮 **Система конкурсов** - ежедневные игры и реферальные конкурсы с призами
-16
View File
@@ -339,12 +339,6 @@ class Settings(BaseSettings):
NALOGO_STORAGE_PATH: str = './nalogo_tokens.json'
AUTO_PURCHASE_AFTER_TOPUP_ENABLED: bool = False
AUTO_ACTIVATE_AFTER_TOPUP_ENABLED: bool = False
# Показывать предупреждение об активации подписки после пополнения баланса
# Если True - после пополнения показывает большое сообщение с кнопками:
# "Активировать", "Продлить", "Добавить устройства"
SHOW_ACTIVATION_PROMPT_AFTER_TOPUP: bool = False
# Отключение превью ссылок в сообщениях бота
DISABLE_WEB_PAGE_PREVIEW: bool = False
@@ -1183,16 +1177,6 @@ class Settings(BaseSettings):
return bool(value)
def is_auto_activate_after_topup_enabled(self) -> bool:
"""Умная автоактивация после пополнения баланса (без корзины)."""
value = getattr(self, 'AUTO_ACTIVATE_AFTER_TOPUP_ENABLED', False)
if isinstance(value, str):
normalized = value.strip().lower()
return normalized in {'1', 'true', 'yes', 'on'}
return bool(value)
def is_quick_amount_buttons_enabled(self) -> bool:
"""Показывать ли кнопки быстрого выбора суммы пополнения."""
return self.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not self.DISABLE_TOPUP_BUTTONS
+19
View File
@@ -95,6 +95,25 @@ async def create_trial_subscription(
end_date = datetime.utcnow() + timedelta(days=duration_days)
# Check for existing PENDING trial subscription (retry after failed payment)
existing = await get_subscription_by_user_id(db, user_id)
if existing and existing.is_trial and existing.status == SubscriptionStatus.PENDING.value:
existing.status = SubscriptionStatus.ACTIVE.value
existing.start_date = datetime.utcnow()
existing.end_date = end_date
existing.traffic_limit_gb = traffic_limit_gb
existing.device_limit = device_limit
existing.connected_squads = final_squads
existing.tariff_id = tariff_id
await db.commit()
await db.refresh(existing)
logger.info(
'🎁 Обновлена PENDING триальная подписка %s для пользователя %s',
existing.id,
user_id,
)
return existing
subscription = Subscription(
user_id=user_id,
status=SubscriptionStatus.ACTIVE.value,
+14 -1
View File
@@ -2048,9 +2048,22 @@ async def required_sub_channel_check(
)
await state.set_state(RegistrationStates.waiting_for_rules_accept)
except TelegramBadRequest as e:
error_msg = str(e).lower()
if 'query is too old' in error_msg or 'query id is invalid' in error_msg:
logger.debug('Устаревший callback в required_sub_channel_check, игнорируем')
else:
logger.error(f'Ошибка Telegram API в required_sub_channel_check: {e}')
try:
await query.answer(f'{texts.ERROR}!', show_alert=True)
except Exception:
pass
except Exception as e:
logger.error(f'Ошибка в required_sub_channel_check: {e}')
await query.answer(f'{texts.ERROR}!', show_alert=True)
try:
await query.answer(f'{texts.ERROR}!', show_alert=True)
except Exception:
pass
def register_handlers(dp: Dispatcher):
+1
View File
@@ -416,6 +416,7 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
).format(
amount=texts.format_price(price),
period=period_label,
months=period_label,
)
if total_discount > 0:
cost_text += texts.t(
+27 -13
View File
@@ -3227,6 +3227,9 @@ async def handle_trial_pay_with_balance(callback: types.CallbackQuery, db_user:
await db.refresh(db_user)
# Сохраняем ID до начала транзакции (на случай detached session)
user_id_snapshot = db_user.id
# Создаем триальную подписку
subscription: Subscription | None = None
remnawave_user = None
@@ -3388,22 +3391,33 @@ async def handle_trial_pay_with_balance(callback: types.CallbackQuery, db_user:
except Exception as error:
logger.error(
'Unexpected error during paid trial activation for user %s: %s',
db_user.id,
user_id_snapshot,
error,
)
# Пытаемся откатить и вернуть деньги
if subscription:
await rollback_trial_subscription_activation(db, subscription)
from app.database.crud.user import add_user_balance
# Откатываем сессию чтобы очистить PendingRollbackError
try:
await db.rollback()
except Exception:
pass
await add_user_balance(
db,
db_user,
trial_price_kopeks,
texts.t('TRIAL_REFUND_DESCRIPTION', 'Возврат за неудачную активацию триала'),
transaction_type=TransactionType.REFUND,
)
await db.refresh(db_user)
# Пытаемся вернуть деньги
try:
from app.database.crud.user import add_user_balance
await add_user_balance(
db,
db_user,
trial_price_kopeks,
texts.t('TRIAL_REFUND_DESCRIPTION', 'Возврат за неудачную активацию триала'),
transaction_type=TransactionType.REFUND,
)
await db.refresh(db_user)
except Exception as refund_error:
logger.error(
'Failed to refund trial payment for user %s: %s',
user_id_snapshot,
refund_error,
)
await callback.message.edit_text(
texts.t(
+3
View File
@@ -1548,6 +1548,9 @@
"TRIAL_PROVISIONING_FAILED": "We couldn't finish setting up the trial. Any charge has been refunded. Please try again later.",
"TRIAL_ROLLBACK_FAILED": "We couldn't cancel the trial activation after a payment error. Please contact support and try again later.",
"TRIAL_REFUND_FAILED": "We couldn't refund the trial activation charge. Please contact support immediately.",
"TRIAL_PAYMENT_DESCRIPTION": "Trial subscription payment",
"TRIAL_REFUND_DESCRIPTION": "Refund for failed trial activation",
"TRIAL_ACTIVATION_ERROR": "❌ An error occurred during trial activation. Funds have been returned to your balance.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 {amount} has been deducted from your balance.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 <b>Access paused</b>\n\nWe couldn't find your subscription to our channel, so the trial plan has been disabled.\n\nJoin the channel and tap “{check_button}” to restore access.",
"TRIAL_ENDING_SOON": "\n🎁 <b>The trial subscription is ending soon!</b>\n\nYour trial expires in a few hours.\n\n💎 <b>Don't want to lose VPN access?</b>\nSwitch to the full subscription!\n\n🔥 <b>Special offer:</b>\n• 30 days for {price}\n• Unlimited traffic\n• All servers available\n• Speeds up to 1 Gbit/s\n\n⚡️ Activate before the trial ends!\n",
+3
View File
@@ -1569,6 +1569,9 @@
"TRIAL_PROVISIONING_FAILED": "فعال‌سازی آزمایشی ناموفق. وجه بازگشت داده شد. بعداً تلاش کنید.",
"TRIAL_ROLLBACK_FAILED": "لغو آزمایشی پس از خطا ناموفق. با پشتیبانی تماس بگیرید.",
"TRIAL_REFUND_FAILED": "بازگشت وجه آزمایشی ناموفق. فوراً با پشتیبانی تماس بگیرید.",
"TRIAL_PAYMENT_DESCRIPTION": "پرداخت اشتراک آزمایشی",
"TRIAL_REFUND_DESCRIPTION": "بازگشت وجه به دلیل خطا در فعال‌سازی آزمایشی",
"TRIAL_ACTIVATION_ERROR": "❌ خطا در فعال‌سازی آزمایشی. وجه به موجودی بازگردانده شد.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 از موجودی شما {amount} کسر شد.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 <b>دسترسی متوقف شد</b>\n\nعضویت شما در کانال یافت نشد، آزمایشی غیرفعال شد.\n\nعضو شوید و «{check_button}» بزنید.",
"TRIAL_ENDING_SOON": "\n🎁 <b>آزمایشی به‌زودی تمام می‌شود!</b>\n\nچند ساعت تا انقضا مانده.\n\n💎 <b>بدون VPN نمانید!</b>\n\n🔥 <b>پیشنهاد ویژه:</b>\n• ۳۰ روز فقط {price}\n• ترافیک نامحدود\n• همه سرورها\n• سرعت تا ۱ گیگ\n\n⚡️ قبل از پایان اقدام کنید!\n",
+3
View File
@@ -1569,6 +1569,9 @@
"TRIAL_PROVISIONING_FAILED": "Не удалось завершить активацию триала. Средства возвращены на баланс. Попробуйте позже.",
"TRIAL_ROLLBACK_FAILED": "Не удалось отменить активацию триала после ошибки списания. Свяжитесь с поддержкой и попробуйте позже.",
"TRIAL_REFUND_FAILED": "Не удалось вернуть оплату за активацию триала. Немедленно свяжитесь с поддержкой.",
"TRIAL_PAYMENT_DESCRIPTION": "Оплата пробной подписки",
"TRIAL_REFUND_DESCRIPTION": "Возврат за неудачную активацию триала",
"TRIAL_ACTIVATION_ERROR": "❌ Произошла ошибка при активации триала. Средства возвращены на баланс.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 С вашего баланса списано {amount}.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 <b>Доступ приостановлен</b>\n\nМы не нашли вашу подписку на наш канал, поэтому тестовая подписка отключена.\n\nПодпишитесь на канал и нажмите «{check_button}», чтобы вернуть доступ.",
"TRIAL_ENDING_SOON": "\n🎁 <b>Тестовая подписка скоро закончится!</b>\n\nВаша тестовая подписка истекает через несколько часов.\n\n💎 <b>Не хотите остаться без VPN?</b>\nПереходите на полную подписку!\n\n🔥 <b>Специальное предложение:</b>\n• 30 дней всего за {price}\n• Безлимитный трафик \n• Все серверы доступны\n• Скорость до 1ГБит/сек\n\n⚡️ Успейте оформить до окончания тестового периода!\n",
+3
View File
@@ -1479,6 +1479,9 @@
"TRIAL_PROVISIONING_FAILED": "Не вдалося завершити активацію тріалу. Кошти повернуто на баланс. Спробуйте пізніше.",
"TRIAL_ROLLBACK_FAILED": "Не вдалося скасувати активацію тріалу після помилки списання. Зв'яжіться з підтримкою і спробуйте пізніше.",
"TRIAL_REFUND_FAILED": "Не вдалося повернути оплату за активацію тріалу. Негайно зв'яжіться з підтримкою.",
"TRIAL_PAYMENT_DESCRIPTION": "Оплата пробної підписки",
"TRIAL_REFUND_DESCRIPTION": "Повернення за невдалу активацію тріалу",
"TRIAL_ACTIVATION_ERROR": "❌ Виникла помилка при активації тріалу. Кошти повернуто на баланс.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 З вашого балансу списано {amount}.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 <b>Доступ призупинено</b>\n\nМи не знайшли вашу підписку на наш канал, тому тестову підписку вимкнено.\n\nПідпишіться на канал і натисніть «{check_button}», щоб повернути доступ.",
"TRIAL_ENDING_SOON": "\n🎁 <b>Тестова підписка скоро закінчиться!</b>\n\nВаша тестова підписка закінчується через декілька годин.\n\n💎 <b>Не хочете залишитися без VPN?</b>\nПереходьте на повну підписку!\n\n🔥 <b>Спеціальна пропозиція:</b>\n• 30 днів усього за {price}\n• Безлімітний трафік  \n• Всі сервери доступні\n• Швидкість до 1ГБіт/сек\n\n⚡️ Встигніть оформити до закінчення тестового періоду!\n",
+6
View File
@@ -1477,6 +1477,9 @@
"TRIAL_PROVISIONING_FAILED":"未能完成试用激活。资金已退回余额。请稍后再试。",
"TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。",
"TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。",
"TRIAL_PAYMENT_DESCRIPTION":"试用订阅付款",
"TRIAL_REFUND_DESCRIPTION":"试用激活失败退款",
"TRIAL_ACTIVATION_ERROR":"❌ 试用激活出错。资金已退回余额。",
"TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。",
"TRIAL_CHANNEL_UNSUBSCRIBED":"\n🚫<b>访问已暂停</b>\n\n我们未找到您对我们频道的订阅,因此试用订阅已禁用。\n\n请订阅频道并点击“{check_button}”以恢复访问。",
"TRIAL_ENDING_SOON":"\n🎁<b>试用订阅即将结束!</b>\n\n您的试用订阅将在几小时后过期。\n\n💎<b>不想没有VPN吗?</b>\n升级到完整订阅!\n\n🔥<b>特别优惠:</b>\n•30天仅需{price}\n•无限流量 \n•所有服务器可用\n•速度高达1Gbit/s\n\n⚡️赶在试用期结束前订购吧!\n",
@@ -1807,6 +1810,9 @@
"TRIAL_PROVISIONING_FAILED":"未能完成试用激活。资金已退回余额。请稍后再试。",
"TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。",
"TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。",
"TRIAL_PAYMENT_DESCRIPTION":"试用订阅付款",
"TRIAL_REFUND_DESCRIPTION":"试用激活失败退款",
"TRIAL_ACTIVATION_ERROR":"❌ 试用激活出错。资金已退回余额。",
"TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。",
"TRIAL_CHANNEL_UNSUBSCRIBED":"\n🚫<b>访问已暂停</b>\n\n我们未找到您对我们频道的订阅,因此试用订阅已禁用。\n\n请订阅频道并点击“{check_button}”以恢复访问。",
"TRIAL_ENDING_SOON":"\n🎁<b>试用订阅即将结束!</b>\n\n您的试用订阅将在几小时后过期。\n\n💎<b>不想没有VPN吗?</b>\n升级到完整订阅!\n\n🔥<b>特别优惠:</b>\n•30天仅需{price}\n•无限流量 \n•所有服务器可用\n•速度高达1Gbit/s\n\n⚡️赶在试用期结束前订购吧!\n",
+1 -13
View File
@@ -12,7 +12,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.cloudpayments_service import CloudPaymentsAPIError
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -262,22 +261,11 @@ class CloudPaymentsPaymentMixin:
logger.exception('Ошибка отправки уведомления CloudPayments: %s', error)
# Auto-purchase if enabled
auto_purchase_success = False
try:
auto_purchase_success = await auto_purchase_saved_cart_after_topup(db, user, bot=getattr(self, 'bot', None))
await auto_purchase_saved_cart_after_topup(db, user, bot=getattr(self, 'bot', None))
except Exception as error:
logger.exception('Ошибка автопокупки после CloudPayments: %s', error)
# Умная автоактивация если автопокупка не сработала
if not auto_purchase_success:
try:
# Игнорируем notification_sent т.к. здесь нет дополнительных уведомлений
await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, 'bot', None), topup_amount=amount_kopeks
)
except Exception as error:
logger.exception('Ошибка умной автоактивации после CloudPayments: %s', error)
return True
async def process_cloudpayments_fail_webhook(
+12 -73
View File
@@ -171,79 +171,18 @@ class PaymentCommonMixin:
try:
payment_method = payment_method_title or 'Банковская карта (YooKassa)'
# Проверяем, нужно ли показывать яркое предупреждение об активации
if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
# Определяем статус подписки для выбора правильной кнопки
has_active_subscription = False
if user_snapshot:
try:
subscription = user_snapshot.subscription
has_active_subscription = bool(
subscription
and not getattr(subscription, 'is_trial', False)
and getattr(subscription, 'is_active', False)
)
except Exception:
pass
# Яркое сообщение с восклицательными знаками
message = (
'✅ <b>Платеж успешно завершен!</b>\n\n'
f'💰 Сумма: {settings.format_price(amount_kopeks)}\n'
f'💳 Способ: {payment_method}\n\n'
'💎 Средства зачислены на ваш баланс!\n\n'
'‼️ <b>ВНИМАНИЕ! ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ!</b> ‼️\n\n'
'⚠️ Пополнение баланса <b>НЕ АКТИВИРУЕТ</b> подписку автоматически!\n\n'
'👇 <b>НАЖМИТЕ КНОПКУ НИЖЕ ДЛЯ АКТИВАЦИИ</b> 👇'
)
# Формируем клавиатуру с кнопками действий
keyboard_rows: list[list[InlineKeyboardButton]] = []
# Кнопка активации или продления в зависимости от статуса
if has_active_subscription:
# Активная платная подписка - показываем продление и изменение устройств
keyboard_rows.append(
[
build_miniapp_or_callback_button(
text='🔄 ПРОДЛИТЬ ПОДПИСКУ',
callback_data='subscription_extend',
)
]
)
keyboard_rows.append(
[
build_miniapp_or_callback_button(
text='📱 Изменить количество устройств',
callback_data='subscription_change_devices',
)
]
)
else:
# Нет подписки или истекла - показываем только активацию
keyboard_rows.append(
[
build_miniapp_or_callback_button(
text='🔥 АКТИВИРОВАТЬ ПОДПИСКУ',
callback_data='menu_buy',
)
]
)
keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
else:
# Стандартное сообщение с полной клавиатурой
keyboard = await self.build_topup_success_keyboard(user_snapshot)
message = (
'✅ <b>Платеж успешно завершен!</b>\n\n'
f'💰 Сумма: {settings.format_price(amount_kopeks)}\n'
f'💳 Способ: {payment_method}\n\n'
'Средства зачислены на ваш баланс!\n\n'
'⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. '
'Обязательно активируйте подписку отдельно!\n\n'
f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
f'подписка будет приобретена автоматически после пополнения баланса.'
)
# Стандартное сообщение с полной клавиатурой
keyboard = await self.build_topup_success_keyboard(user_snapshot)
message = (
'✅ <b>Платеж успешно завершен!</b>\n\n'
f'💰 Сумма: {settings.format_price(amount_kopeks)}\n'
f'💳 Способ: {payment_method}\n\n'
'Средства зачислены на ваш баланс!\n\n'
'⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. '
'Обязательно активируйте подписку отдельно!\n\n'
f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
f'подписка будет приобретена автоматически после пополнения баланса.'
)
await self.bot.send_message(
chat_id=telegram_id,
+1 -21
View File
@@ -14,7 +14,6 @@ from app.config import settings
from app.database.database import AsyncSessionLocal
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.services.subscription_renewal_service import (
@@ -361,26 +360,7 @@ class CryptoBotPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db,
user,
bot=bot_instance,
topup_amount=amount_kopeks,
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and bot_instance and not activation_notification_sent:
if has_saved_cart and bot_instance:
from app.localization.texts import get_texts
texts = get_texts(user.language)
+1 -18
View File
@@ -14,7 +14,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.freekassa_service import freekassa_service
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -388,23 +387,7 @@ class FreekassaPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
-17
View File
@@ -13,7 +13,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -452,22 +451,6 @@ class HeleketPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
if not auto_purchase_success:
try:
await auto_activate_subscription_after_topup(
db,
user,
bot=getattr(self, 'bot', None),
topup_amount=amount_kopeks,
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
except Exception as error:
logger.error(
'Ошибка при работе с автоактивацией для пользователя %s: %s',
+9 -46
View File
@@ -14,7 +14,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.kassa_ai_service import kassa_ai_service
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -339,34 +338,14 @@ class KassaAiPaymentMixin:
try:
display_name = settings.get_kassa_ai_display_name()
if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
# Яркое сообщение для тупых
from aiogram import types
message = (
'✅ <b>Платеж успешно завершен!</b>\n\n'
f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n'
f'💳 Способ: {display_name}\n\n'
'💎 Средства зачислены на ваш баланс!\n\n'
'‼️ <b>ВНИМАНИЕ! ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ!</b> ‼️\n\n'
'⚠️ Пополнение баланса <b>НЕ АКТИВИРУЕТ</b> подписку автоматически!\n\n'
'👇 <b>НАЖМИТЕ КНОПКУ НИЖЕ ДЛЯ АКТИВАЦИИ</b> 👇'
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='🔥 АКТИВИРОВАТЬ ПОДПИСКУ', callback_data='menu_buy')],
]
)
else:
# Стандартное сообщение (как было раньше)
keyboard = await self.build_topup_success_keyboard(user)
message = (
'✅ <b>Пополнение успешно!</b>\n\n'
f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n'
f'💳 Способ: {display_name}\n'
f'🆔 Транзакция: {transaction.id}\n\n'
'Баланс пополнен автоматически!'
)
keyboard = await self.build_topup_success_keyboard(user)
message = (
'✅ <b>Пополнение успешно!</b>\n\n'
f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n'
f'💳 Способ: {display_name}\n'
f'🆔 Транзакция: {transaction.id}\n\n'
'Баланс пополнен автоматически!'
)
await self.bot.send_message(
user.telegram_id,
@@ -404,23 +383,7 @@ class KassaAiPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
+1 -23
View File
@@ -11,7 +11,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -390,28 +389,7 @@ class MulenPayPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили и есть telegram_id
if (
has_saved_cart
and getattr(self, 'bot', None)
and not activation_notification_sent
and user.telegram_id
):
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
# Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению
from app.localization.texts import get_texts
+1 -18
View File
@@ -13,7 +13,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.pal24_service import Pal24APIError
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -489,23 +488,7 @@ class Pal24PaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
+1 -18
View File
@@ -13,7 +13,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.platega_service import PlategaService
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -470,23 +469,7 @@ class PlategaPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
+1 -21
View File
@@ -19,7 +19,6 @@ from app.database.crud.user import get_user_by_id
from app.database.models import PaymentMethod, TransactionType
from app.external.telegram_stars import TelegramStarsService
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -534,26 +533,7 @@ class TelegramStarsMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db,
user,
bot=getattr(self, 'bot', None),
topup_amount=amount_kopeks,
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
texts = get_texts(user.language)
cart_message = texts.t(
'BALANCE_TOPUP_CART_REMINDER_DETAILED',
+1 -18
View File
@@ -12,7 +12,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.services.wata_service import WataAPIError, WataService
@@ -575,23 +574,7 @@ class WataPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
+49 -69
View File
@@ -16,7 +16,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -847,78 +846,59 @@ class YooKassaPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
if not auto_purchase_success:
try:
await auto_activate_subscription_after_topup(
db,
user,
bot=getattr(self, 'bot', None),
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
# Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению
from aiogram import types
# Если включен яркий промпт активации, пропускаем старое уведомление
# т.к. оно будет отправлено через _send_payment_success_notification
if not settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
# Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению
from aiogram import types
from app.localization.texts import get_texts
from app.localization.texts import get_texts
texts = get_texts(user.language)
cart_message = texts.BALANCE_TOPUP_CART_REMINDER_DETAILED.format(
total_amount=settings.format_price(payment.amount_kopeks)
)
texts = get_texts(user.language)
cart_message = texts.BALANCE_TOPUP_CART_REMINDER_DETAILED.format(
total_amount=settings.format_price(payment.amount_kopeks)
)
# Создаем клавиатуру с кнопками
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data='return_to_saved_cart',
)
],
[
types.InlineKeyboardButton(
text='💰 Мой баланс',
callback_data='menu_balance',
)
],
[
types.InlineKeyboardButton(
text='🏠 Главное меню',
callback_data='back_to_menu',
)
],
]
)
# Создаем клавиатуру с кнопками
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data='return_to_saved_cart',
)
],
[
types.InlineKeyboardButton(
text='💰 Мой баланс',
callback_data='menu_balance',
)
],
[
types.InlineKeyboardButton(
text='🏠 Главное меню',
callback_data='back_to_menu',
)
],
]
)
await self.bot.send_message(
chat_id=user.telegram_id,
text=f'✅ Баланс пополнен на {settings.format_price(payment.amount_kopeks)}!\n\n'
f'⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. '
f'Обязательно активируйте подписку отдельно!\n\n'
f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
f'подписка будет приобретена автоматически после пополнения баланса.\n\n{cart_message}',
reply_markup=keyboard,
)
logger.info(
f'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю {user.id}'
)
else:
logger.info(
'У пользователя %s нет сохраненной корзины, бот недоступен или покупка уже выполнена',
user.id,
)
await self.bot.send_message(
chat_id=user.telegram_id,
text=f'✅ Баланс пополнен на {settings.format_price(payment.amount_kopeks)}!\n\n'
f'⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. '
f'Обязательно активируйте подписку отдельно!\n\n'
f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
f'подписка будет приобретена автоматически после пополнения баланса.\n\n{cart_message}',
reply_markup=keyboard,
)
logger.info(
f'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю {user.id}'
)
else:
logger.info(
'У пользователя %s нет сохраненной корзины, бот недоступен или покупка уже выполнена',
user.id,
)
except Exception as e:
logger.error(
f'Критическая ошибка при работе с сохраненной корзиной для пользователя {user.id}: {e}',
@@ -1814,340 +1814,4 @@ async def auto_purchase_saved_cart_after_topup(
return True
async def auto_activate_subscription_after_topup(
db: AsyncSession,
user: User,
*,
bot: Bot | None = None,
topup_amount: int | None = None,
) -> tuple[bool, bool]:
"""
Умная автоактивация после пополнения баланса.
Работает БЕЗ сохранённой корзины:
- Если подписка активна ничего не делает
- Если подписка истекла продлевает с теми же параметрами
- Если подписки нет создаёт новую с дефолтными параметрами
Выбирает максимальный период, который можно оплатить из баланса.
Args:
topup_amount: Сумма пополнения в копейках (для отображения в уведомлении)
Returns:
tuple[bool, bool]: (success, notification_sent)
- success: True если подписка активирована
- notification_sent: True если уведомление отправлено пользователю
"""
from datetime import datetime
# Lazy imports to avoid circular dependency
from app.cabinet.routes.websocket import (
notify_user_subscription_activated,
notify_user_subscription_renewed,
)
from app.database.crud.server_squad import get_available_server_squads, get_server_ids_by_uuids
from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id
from app.database.crud.transaction import create_transaction
from app.database.crud.user import subtract_user_balance
from app.database.models import PaymentMethod, TransactionType
from app.services.admin_notification_service import AdminNotificationService
from app.services.subscription_renewal_service import SubscriptionRenewalService
from app.services.subscription_service import SubscriptionService
if not user or not getattr(user, 'id', None):
return (False, False)
subscription = await get_subscription_by_user_id(db, user.id)
# Если автоактивация отключена - уведомление отправится из _send_payment_success_notification
if not settings.is_auto_activate_after_topup_enabled():
logger.info(
'⚠️ Автоактивация отключена для пользователя %s, уведомление будет отправлено из payment service',
_format_user_id(user),
)
return (False, False)
# Если подписка активна — ничего не делаем (автоактивация включена, но подписка уже есть)
if subscription and subscription.status == 'ACTIVE' and subscription.end_date > datetime.utcnow():
logger.info(
'🔁 Автоактивация: у пользователя %s уже активная подписка, пропускаем',
_format_user_id(user),
)
return (False, False)
# Определяем параметры подписки
if subscription:
device_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
# В режиме fixed_with_topup при автоактивации используем фиксированный лимит
if settings.is_traffic_fixed():
traffic_limit_gb = settings.get_fixed_traffic_limit()
else:
traffic_limit_gb = subscription.traffic_limit_gb or 0
connected_squads = subscription.connected_squads or []
else:
device_limit = settings.DEFAULT_DEVICE_LIMIT
# В режиме fixed_with_topup при автоактивации используем фиксированный лимит
if settings.is_traffic_fixed():
traffic_limit_gb = settings.get_fixed_traffic_limit()
else:
traffic_limit_gb = 0
connected_squads = []
# Если серверы не выбраны — берём бесплатные по умолчанию
if not connected_squads:
available_servers = await get_available_server_squads(db, promo_group_id=user.promo_group_id)
connected_squads = [s.squad_uuid for s in available_servers if s.is_available and s.price_kopeks == 0]
if not connected_squads and available_servers:
connected_squads = [available_servers[0].squad_uuid]
server_ids = await get_server_ids_by_uuids(db, connected_squads) if connected_squads else []
balance = user.balance_kopeks
available_periods = sorted(settings.get_available_subscription_periods(), reverse=True)
if not available_periods:
logger.warning('🔁 Автоактивация: нет доступных периодов подписки')
return (False, False)
subscription_service = SubscriptionService()
# Найти максимальный период <= баланса
best_period = None
best_price = 0
for period in available_periods:
try:
price, _ = await subscription_service.calculate_subscription_price_with_months(
period, traffic_limit_gb, server_ids, device_limit, db, user=user
)
if price <= balance:
best_period = period
best_price = price
break
except Exception as calc_error:
logger.warning(
'🔁 Автоактивация: ошибка расчёта цены для периода %s: %s',
period,
calc_error,
)
continue
if not best_period:
logger.info(
'🔁 Автоактивация: у пользователя %s недостаточно средств (%s) для любого периода',
_format_user_id(user),
balance,
)
# Уведомление отправится из _send_payment_success_notification
logger.info(
'⚠️ Недостаточно средств для автоактивации пользователя %s, уведомление будет отправлено из payment service',
_format_user_id(user),
)
return (False, False)
texts = get_texts(getattr(user, 'language', 'ru'))
try:
if subscription:
# Продление существующей подписки
renewal_service = SubscriptionRenewalService()
pricing = await renewal_service.calculate_pricing(db, user, subscription, best_period)
result = await renewal_service.finalize(
db,
user,
subscription,
pricing,
description=f'Автоматическое продление на {best_period} дней',
payment_method=PaymentMethod.BALANCE,
)
logger.info(
'✅ Автоактивация: подписка пользователя %s продлена на %s дней за %s коп.',
_format_user_id(user),
best_period,
best_price,
)
# Send WebSocket notification to cabinet frontend
try:
await notify_user_subscription_renewed(
user_id=user.id,
new_expires_at=result.subscription.end_date.isoformat() if result.subscription.end_date else '',
amount_kopeks=best_price,
)
except Exception as ws_error:
logger.warning(
'⚠️ Автоактивация: не удалось отправить WS уведомление о продлении для %s: %s',
_format_user_id(user),
ws_error,
)
# Уведомление пользователю (только для Telegram-пользователей)
if bot and user.telegram_id:
try:
period_label = format_period_description(best_period, getattr(user, 'language', 'ru'))
new_end_date = result.subscription.end_date
end_date_str = new_end_date.strftime('%d.%m.%Y') if new_end_date else ''
message = texts.t(
'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED',
'✅ Подписка автоматически продлена на {period}.',
).format(period=period_label)
details = texts.t(
'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED_DETAILS',
'⏰ Новая дата окончания: {date}.',
).format(date=end_date_str)
hint = texts.t(
'AUTO_PURCHASE_SUBSCRIPTION_HINT',
'Перейдите в раздел «Моя подписка», чтобы получить ссылку.',
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=texts.t('MY_SUBSCRIPTION_BUTTON', '📱 Моя подписка'),
callback_data='menu_subscription',
)
],
]
)
await bot.send_message(
chat_id=user.telegram_id,
text=f'{message}\n{details}\n\n{hint}',
reply_markup=keyboard,
parse_mode='HTML',
)
except Exception as notify_error:
logger.warning(
'⚠️ Автоактивация: не удалось уведомить пользователя %s: %s',
user.telegram_id or user.id,
notify_error,
)
else:
# Создание новой подписки
new_subscription = await create_paid_subscription(
db,
user.id,
best_period,
traffic_limit_gb=traffic_limit_gb,
device_limit=device_limit,
connected_squads=connected_squads,
update_server_counters=True,
)
await subtract_user_balance(db, user, best_price, f'Активация подписки на {best_period} дней')
await subscription_service.create_remnawave_user(db, new_subscription)
await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=best_price,
description=f'Активация подписки на {best_period} дней',
payment_method=PaymentMethod.BALANCE,
)
logger.info(
'✅ Автоактивация: новая подписка на %s дней создана для пользователя %s за %s коп.',
best_period,
_format_user_id(user),
best_price,
)
# Send WebSocket notification to cabinet frontend
try:
await notify_user_subscription_activated(
user_id=user.id,
expires_at=new_subscription.end_date.isoformat() if new_subscription.end_date else '',
tariff_name='',
)
except Exception as ws_error:
logger.warning(
'⚠️ Автоактивация: не удалось отправить WS уведомление об активации для %s: %s',
_format_user_id(user),
ws_error,
)
# Уведомление пользователю (только для Telegram-пользователей)
if bot and user.telegram_id:
try:
period_label = format_period_description(best_period, getattr(user, 'language', 'ru'))
message = texts.t(
'AUTO_PURCHASE_SUBSCRIPTION_SUCCESS',
'✅ Подписка на {period} автоматически оформлена после пополнения баланса.',
).format(period=period_label)
hint = texts.t(
'AUTO_PURCHASE_SUBSCRIPTION_HINT',
'Перейдите в раздел «Моя подписка», чтобы получить ссылку.',
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=texts.t('MY_SUBSCRIPTION_BUTTON', '📱 Моя подписка'),
callback_data='menu_subscription',
)
],
]
)
await bot.send_message(
chat_id=user.telegram_id,
text=f'{message}\n\n{hint}',
reply_markup=keyboard,
parse_mode='HTML',
)
except Exception as notify_error:
logger.warning(
'⚠️ Автоактивация: не удалось уведомить пользователя %s: %s',
user.telegram_id or user.id,
notify_error,
)
# Уведомление админам (независимо от telegram_id)
if bot:
try:
notification_service = AdminNotificationService(bot)
await notification_service.send_subscription_purchase_notification(
db,
user,
new_subscription,
None, # transaction
best_period,
False, # was_trial_conversion
)
except Exception as admin_error:
logger.warning(
'⚠️ Автоактивация: не удалось уведомить админов: %s',
admin_error,
)
return (True, True) # success=True, notification_sent=True (об активации)
except Exception as e:
logger.error(
'❌ Автоактивация: ошибка для пользователя %s: %s',
_format_user_id(user),
e,
exc_info=True,
)
try:
await db.rollback()
except Exception:
pass
return (False, False)
__all__ = ['auto_activate_subscription_after_topup', 'auto_purchase_saved_cart_after_topup']
__all__ = ['auto_purchase_saved_cart_after_topup']
-14
View File
@@ -260,7 +260,6 @@ class BotConfigurationService:
'PAYMENT_BALANCE_TEMPLATE': 'PAYMENT',
'PAYMENT_SUBSCRIPTION_TEMPLATE': 'PAYMENT',
'AUTO_PURCHASE_AFTER_TOPUP_ENABLED': 'PAYMENT',
'SHOW_ACTIVATION_PROMPT_AFTER_TOPUP': 'PAYMENT',
'SIMPLE_SUBSCRIPTION_ENABLED': 'SIMPLE_SUBSCRIPTION',
'SIMPLE_SUBSCRIPTION_PERIOD_DAYS': 'SIMPLE_SUBSCRIPTION',
'SIMPLE_SUBSCRIPTION_DEVICE_LIMIT': 'SIMPLE_SUBSCRIPTION',
@@ -585,19 +584,6 @@ class BotConfigurationService:
'example': 'true',
'warning': ('Используйте с осторожностью: средства будут списаны мгновенно, если корзина найдена.'),
},
'SHOW_ACTIVATION_PROMPT_AFTER_TOPUP': {
'description': (
'Включает режим яркого промпта активации подписки после пополнения баланса. '
'Вместо обычного уведомления пользователь получит яркое сообщение с восклицательными знаками '
'и кнопками для активации/продления подписки или изменения количества устройств.'
),
'format': 'Булево значение.',
'example': 'true',
'warning': (
'При включении пользователи будут получать только яркое уведомление без кнопок баланса и главного меню. '
'Эти кнопки появятся после выполнения действия (активация/продление/изменение устройств).'
),
},
'SUPPORT_TICKET_SLA_MINUTES': {
'description': 'Лимит времени для ответа модераторов на тикет в минутах.',
'format': 'Целое число от 1 до 1440.',
+2 -18
View File
@@ -14,7 +14,6 @@ from app.database.models import PaymentMethod, TransactionType
from app.external.tribute import TributeService as TributeAPI
from app.services.payment_service import PaymentService
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.user_utils import format_referrer_info
@@ -307,23 +306,8 @@ class TributeService:
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
session, user, bot=self.bot, topup_amount=amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили и есть telegram_id
if has_saved_cart and self.bot and not activation_notification_sent and user_id:
# Отправляем уведомление только если есть сохранённая корзина и telegram_id
if has_saved_cart and self.bot and user_id:
# Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению
from aiogram import types
+10 -2
View File
@@ -2,7 +2,7 @@ import asyncio
import logging
from aiogram import types
from aiogram.exceptions import TelegramBadRequest, TelegramNetworkError
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramNetworkError
from aiogram.types import InaccessibleMessage, InputMediaPhoto
from app.config import settings
@@ -126,6 +126,8 @@ async def edit_or_answer_photo(
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
except TelegramForbiddenError:
logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error:
try:
await callback.message.delete()
@@ -140,6 +142,8 @@ async def edit_or_answer_photo(
if callback.message.photo:
await callback.message.delete()
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
except TelegramForbiddenError:
logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error:
await _answer_text(callback, caption, keyboard, resolved_parse_mode, error)
return
@@ -167,6 +171,10 @@ async def edit_or_answer_photo(
pass
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
return
except TelegramForbiddenError:
# Пользователь заблокировал бота — молча игнорируем
logger.debug('Пользователь заблокировал бота, пропускаем edit_media')
return
except TelegramBadRequest as error:
if is_privacy_restricted_error(error):
try:
@@ -189,7 +197,7 @@ async def edit_or_answer_photo(
parse_mode=resolved_parse_mode,
)
_cache_logo_file_id(result)
except TelegramBadRequest as photo_error:
except (TelegramBadRequest, TelegramForbiddenError) as photo_error:
await _answer_text(callback, caption, keyboard, resolved_parse_mode, photo_error)
except Exception:
# Последний фоллбек — обычный текст
@@ -5,71 +5,6 @@
from unittest.mock import MagicMock
def test_notification_message_bright_prompt():
"""
Тест: проверяем что формируется ЯРКОЕ сообщение с SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=true.
"""
# Эмулируем код из kassa_ai.py
SHOW_ACTIVATION_PROMPT_AFTER_TOPUP = True
display_name = 'Kassa AI'
amount_formatted = '10₽'
if SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
message = (
'✅ <b>Платеж успешно завершен!</b>\n\n'
f'💰 Сумма: {amount_formatted}\n'
f'💳 Способ: {display_name}\n\n'
'💎 Средства зачислены на ваш баланс!\n\n'
'‼️ <b>ВНИМАНИЕ! ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ!</b> ‼️\n\n'
'⚠️ Пополнение баланса <b>НЕ АКТИВИРУЕТ</b> подписку автоматически!\n\n'
'👇 <b>НАЖМИТЕ КНОПКУ НИЖЕ ДЛЯ АКТИВАЦИИ</b> 👇'
)
else:
message = ''
# Проверки
assert '‼️' in message
assert 'ВНИМАНИЕ' in message
assert 'ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ' in message
assert '👇' in message
assert display_name in message
assert amount_formatted in message
print(f'\n✅ ЯРКОЕ сообщение сформировано правильно:\n{message}')
def test_notification_message_standard():
"""
Тест: проверяем что формируется обычное сообщение с SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=false.
"""
# Эмулируем код из kassa_ai.py
SHOW_ACTIVATION_PROMPT_AFTER_TOPUP = False
display_name = 'Kassa AI'
amount_formatted = '10₽'
if SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
message = ''
else:
message = (
'✅ <b>Платеж успешно завершен!</b>\n\n'
f'💰 Сумма: {amount_formatted}\n'
f'💳 Способ: {display_name}\n\n'
'Средства зачислены на ваш баланс!\n\n'
'⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. '
'Обязательно активируйте подписку отдельно!\n\n'
f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
f'подписка будет приобретена автоматически после пополнения баланса.'
)
# Проверки
assert '‼️' not in message
assert 'ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ' not in message
assert 'Платеж успешно завершен' in message
assert display_name in message
assert amount_formatted in message
print(f'\n✅ Обычное сообщение сформировано правильно:\n{message}')
def test_telegram_id_saved_before_commit():
"""
Тест: проверяем что telegram_id сохраняется в локальную переменную ДО commit.
Generated
+1 -1
View File
@@ -1149,7 +1149,7 @@ wheels = [
[[package]]
name = "remnawave-bedolaga-telegram-bot"
version = "3.6.0"
version = "3.8.0"
source = { virtual = "." }
dependencies = [
{ name = "aiogram" },