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 REFERRAL_CONTESTS_ENABLED=false
# ===== АВТОАКТИВАЦИЯ ПОСЛЕ ПОПОЛНЕНИЯ ===== # ===== АВТОПОКУПКА ПОСЛЕ ПОПОЛНЕНИЯ =====
# Автоматическая покупка из сохранённой корзины после пополнения баланса # Автоматическая покупка из сохранённой корзины после пополнения баланса
AUTO_PURCHASE_AFTER_TOPUP_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 ACTIVATE_BUTTON_VISIBLE=false
-2
View File
@@ -1342,7 +1342,6 @@ CONTEST_BUTTON_VISIBLE=true
- 🔄 Автоплатёж с настройкой дня списания - 🔄 Автоплатёж с настройкой дня списания
- 🎁 Реферальные и промо-бонусы - 🎁 Реферальные и промо-бонусы
-**Быстрое пополнение** с кнопками быстрых сумм -**Быстрое пополнение** с кнопками быстрых сумм
- 🔄 **Умная автоактивация** подписки после пополнения баланса
📱 **Управление подписками** 📱 **Управление подписками**
@@ -1529,7 +1528,6 @@ CONTEST_BUTTON_VISIBLE=true
- 🔄 **Миграция сквадов** - массовый перенос пользователей между сквадами - 🔄 **Миграция сквадов** - массовый перенос пользователей между сквадами
- 🧾 **История операций** - хранение всех транзакций и действий для аудита - 🧾 **История операций** - хранение всех транзакций и действий для аудита
- 💸 **Сервис автопроверки транзакций** - автоматическая проверка транзакций в статусе "В ожидании оплаты" за последние 24ч - 💸 **Сервис автопроверки транзакций** - автоматическая проверка транзакций в статусе "В ожидании оплаты" за последние 24ч
- 🔄 **Умная автоактивация** - автоматическая активация подписки после пополнения баланса
- 📝 **Ротация логов** - автоматическая очистка и архивация старых логов - 📝 **Ротация логов** - автоматическая очистка и архивация старых логов
- 🎮 **Система конкурсов** - ежедневные игры и реферальные конкурсы с призами - 🎮 **Система конкурсов** - ежедневные игры и реферальные конкурсы с призами
-16
View File
@@ -339,12 +339,6 @@ class Settings(BaseSettings):
NALOGO_STORAGE_PATH: str = './nalogo_tokens.json' NALOGO_STORAGE_PATH: str = './nalogo_tokens.json'
AUTO_PURCHASE_AFTER_TOPUP_ENABLED: bool = False 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 DISABLE_WEB_PAGE_PREVIEW: bool = False
@@ -1183,16 +1177,6 @@ class Settings(BaseSettings):
return bool(value) 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: def is_quick_amount_buttons_enabled(self) -> bool:
"""Показывать ли кнопки быстрого выбора суммы пополнения.""" """Показывать ли кнопки быстрого выбора суммы пополнения."""
return self.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not self.DISABLE_TOPUP_BUTTONS 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) 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( subscription = Subscription(
user_id=user_id, user_id=user_id,
status=SubscriptionStatus.ACTIVE.value, 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) 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: except Exception as e:
logger.error(f'Ошибка в required_sub_channel_check: {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): 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( ).format(
amount=texts.format_price(price), amount=texts.format_price(price),
period=period_label, period=period_label,
months=period_label,
) )
if total_discount > 0: if total_discount > 0:
cost_text += texts.t( 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) await db.refresh(db_user)
# Сохраняем ID до начала транзакции (на случай detached session)
user_id_snapshot = db_user.id
# Создаем триальную подписку # Создаем триальную подписку
subscription: Subscription | None = None subscription: Subscription | None = None
remnawave_user = None remnawave_user = None
@@ -3388,22 +3391,33 @@ async def handle_trial_pay_with_balance(callback: types.CallbackQuery, db_user:
except Exception as error: except Exception as error:
logger.error( logger.error(
'Unexpected error during paid trial activation for user %s: %s', 'Unexpected error during paid trial activation for user %s: %s',
db_user.id, user_id_snapshot,
error, error,
) )
# Пытаемся откатить и вернуть деньги # Откатываем сессию чтобы очистить PendingRollbackError
if subscription: try:
await rollback_trial_subscription_activation(db, subscription) await db.rollback()
from app.database.crud.user import add_user_balance except Exception:
pass
await add_user_balance( # Пытаемся вернуть деньги
db, try:
db_user, from app.database.crud.user import add_user_balance
trial_price_kopeks,
texts.t('TRIAL_REFUND_DESCRIPTION', 'Возврат за неудачную активацию триала'), await add_user_balance(
transaction_type=TransactionType.REFUND, db,
) db_user,
await db.refresh(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( await callback.message.edit_text(
texts.t( 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_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_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_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_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_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", "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_PROVISIONING_FAILED": "فعال‌سازی آزمایشی ناموفق. وجه بازگشت داده شد. بعداً تلاش کنید.",
"TRIAL_ROLLBACK_FAILED": "لغو آزمایشی پس از خطا ناموفق. با پشتیبانی تماس بگیرید.", "TRIAL_ROLLBACK_FAILED": "لغو آزمایشی پس از خطا ناموفق. با پشتیبانی تماس بگیرید.",
"TRIAL_REFUND_FAILED": "بازگشت وجه آزمایشی ناموفق. فوراً با پشتیبانی تماس بگیرید.", "TRIAL_REFUND_FAILED": "بازگشت وجه آزمایشی ناموفق. فوراً با پشتیبانی تماس بگیرید.",
"TRIAL_PAYMENT_DESCRIPTION": "پرداخت اشتراک آزمایشی",
"TRIAL_REFUND_DESCRIPTION": "بازگشت وجه به دلیل خطا در فعال‌سازی آزمایشی",
"TRIAL_ACTIVATION_ERROR": "❌ خطا در فعال‌سازی آزمایشی. وجه به موجودی بازگردانده شد.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 از موجودی شما {amount} کسر شد.", "TRIAL_PAYMENT_CHARGED_NOTE": "💳 از موجودی شما {amount} کسر شد.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 <b>دسترسی متوقف شد</b>\n\nعضویت شما در کانال یافت نشد، آزمایشی غیرفعال شد.\n\nعضو شوید و «{check_button}» بزنید.", "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", "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_PROVISIONING_FAILED": "Не удалось завершить активацию триала. Средства возвращены на баланс. Попробуйте позже.",
"TRIAL_ROLLBACK_FAILED": "Не удалось отменить активацию триала после ошибки списания. Свяжитесь с поддержкой и попробуйте позже.", "TRIAL_ROLLBACK_FAILED": "Не удалось отменить активацию триала после ошибки списания. Свяжитесь с поддержкой и попробуйте позже.",
"TRIAL_REFUND_FAILED": "Не удалось вернуть оплату за активацию триала. Немедленно свяжитесь с поддержкой.", "TRIAL_REFUND_FAILED": "Не удалось вернуть оплату за активацию триала. Немедленно свяжитесь с поддержкой.",
"TRIAL_PAYMENT_DESCRIPTION": "Оплата пробной подписки",
"TRIAL_REFUND_DESCRIPTION": "Возврат за неудачную активацию триала",
"TRIAL_ACTIVATION_ERROR": "❌ Произошла ошибка при активации триала. Средства возвращены на баланс.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 С вашего баланса списано {amount}.", "TRIAL_PAYMENT_CHARGED_NOTE": "💳 С вашего баланса списано {amount}.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 <b>Доступ приостановлен</b>\n\nМы не нашли вашу подписку на наш канал, поэтому тестовая подписка отключена.\n\nПодпишитесь на канал и нажмите «{check_button}», чтобы вернуть доступ.", "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", "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_PROVISIONING_FAILED": "Не вдалося завершити активацію тріалу. Кошти повернуто на баланс. Спробуйте пізніше.",
"TRIAL_ROLLBACK_FAILED": "Не вдалося скасувати активацію тріалу після помилки списання. Зв'яжіться з підтримкою і спробуйте пізніше.", "TRIAL_ROLLBACK_FAILED": "Не вдалося скасувати активацію тріалу після помилки списання. Зв'яжіться з підтримкою і спробуйте пізніше.",
"TRIAL_REFUND_FAILED": "Не вдалося повернути оплату за активацію тріалу. Негайно зв'яжіться з підтримкою.", "TRIAL_REFUND_FAILED": "Не вдалося повернути оплату за активацію тріалу. Негайно зв'яжіться з підтримкою.",
"TRIAL_PAYMENT_DESCRIPTION": "Оплата пробної підписки",
"TRIAL_REFUND_DESCRIPTION": "Повернення за невдалу активацію тріалу",
"TRIAL_ACTIVATION_ERROR": "❌ Виникла помилка при активації тріалу. Кошти повернуто на баланс.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 З вашого балансу списано {amount}.", "TRIAL_PAYMENT_CHARGED_NOTE": "💳 З вашого балансу списано {amount}.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 <b>Доступ призупинено</b>\n\nМи не знайшли вашу підписку на наш канал, тому тестову підписку вимкнено.\n\nПідпишіться на канал і натисніть «{check_button}», щоб повернути доступ.", "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", "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_PROVISIONING_FAILED":"未能完成试用激活。资金已退回余额。请稍后再试。",
"TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。", "TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。",
"TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。", "TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。",
"TRIAL_PAYMENT_DESCRIPTION":"试用订阅付款",
"TRIAL_REFUND_DESCRIPTION":"试用激活失败退款",
"TRIAL_ACTIVATION_ERROR":"❌ 试用激活出错。资金已退回余额。",
"TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。", "TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。",
"TRIAL_CHANNEL_UNSUBSCRIBED":"\n🚫<b>访问已暂停</b>\n\n我们未找到您对我们频道的订阅,因此试用订阅已禁用。\n\n请订阅频道并点击“{check_button}”以恢复访问。", "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", "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_PROVISIONING_FAILED":"未能完成试用激活。资金已退回余额。请稍后再试。",
"TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。", "TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。",
"TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。", "TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。",
"TRIAL_PAYMENT_DESCRIPTION":"试用订阅付款",
"TRIAL_REFUND_DESCRIPTION":"试用激活失败退款",
"TRIAL_ACTIVATION_ERROR":"❌ 试用激活出错。资金已退回余额。",
"TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。", "TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。",
"TRIAL_CHANNEL_UNSUBSCRIBED":"\n🚫<b>访问已暂停</b>\n\n我们未找到您对我们频道的订阅,因此试用订阅已禁用。\n\n请订阅频道并点击“{check_button}”以恢复访问。", "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", "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.database.models import PaymentMethod, TransactionType
from app.services.cloudpayments_service import CloudPaymentsAPIError from app.services.cloudpayments_service import CloudPaymentsAPIError
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -262,22 +261,11 @@ class CloudPaymentsPaymentMixin:
logger.exception('Ошибка отправки уведомления CloudPayments: %s', error) logger.exception('Ошибка отправки уведомления CloudPayments: %s', error)
# Auto-purchase if enabled # Auto-purchase if enabled
auto_purchase_success = False
try: 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: except Exception as error:
logger.exception('Ошибка автопокупки после CloudPayments: %s', 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 return True
async def process_cloudpayments_fail_webhook( async def process_cloudpayments_fail_webhook(
+12 -73
View File
@@ -171,79 +171,18 @@ class PaymentCommonMixin:
try: try:
payment_method = payment_method_title or 'Банковская карта (YooKassa)' payment_method = payment_method_title or 'Банковская карта (YooKassa)'
# Проверяем, нужно ли показывать яркое предупреждение об активации # Стандартное сообщение с полной клавиатурой
if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP: keyboard = await self.build_topup_success_keyboard(user_snapshot)
# Определяем статус подписки для выбора правильной кнопки message = (
has_active_subscription = False '✅ <b>Платеж успешно завершен!</b>\n\n'
if user_snapshot: f'💰 Сумма: {settings.format_price(amount_kopeks)}\n'
try: f'💳 Способ: {payment_method}\n\n'
subscription = user_snapshot.subscription 'Средства зачислены на ваш баланс!\n\n'
has_active_subscription = bool( '⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. '
subscription 'Обязательно активируйте подписку отдельно!\n\n'
and not getattr(subscription, 'is_trial', False) f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
and getattr(subscription, 'is_active', False) f'подписка будет приобретена автоматически после пополнения баланса.'
) )
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'подписка будет приобретена автоматически после пополнения баланса.'
)
await self.bot.send_message( await self.bot.send_message(
chat_id=telegram_id, 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.database import AsyncSessionLocal
from app.database.models import PaymentMethod, TransactionType from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.services.subscription_renewal_service import ( from app.services.subscription_renewal_service import (
@@ -361,26 +360,7 @@ class CryptoBotPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and bot_instance:
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:
from app.localization.texts import get_texts from app.localization.texts import get_texts
texts = get_texts(user.language) 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.database.models import PaymentMethod, TransactionType
from app.services.freekassa_service import freekassa_service from app.services.freekassa_service import freekassa_service
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -388,23 +387,7 @@ class FreekassaPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
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:
from app.localization.texts import get_texts from app.localization.texts import get_texts
texts = get_texts(user.language) 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.config import settings
from app.database.models import PaymentMethod, TransactionType from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -452,22 +451,6 @@ class HeleketPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False 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: except Exception as error:
logger.error( logger.error(
'Ошибка при работе с автоактивацией для пользователя %s: %s', 'Ошибка при работе с автоактивацией для пользователя %s: %s',
+9 -46
View File
@@ -14,7 +14,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType from app.database.models import PaymentMethod, TransactionType
from app.services.kassa_ai_service import kassa_ai_service from app.services.kassa_ai_service import kassa_ai_service
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -339,34 +338,14 @@ class KassaAiPaymentMixin:
try: try:
display_name = settings.get_kassa_ai_display_name() display_name = settings.get_kassa_ai_display_name()
if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP: keyboard = await self.build_topup_success_keyboard(user)
# Яркое сообщение для тупых message = (
from aiogram import types '✅ <b>Пополнение успешно!</b>\n\n'
f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n'
message = ( f'💳 Способ: {display_name}\n'
'✅ <b>Платеж успешно завершен!</b>\n\n' f'🆔 Транзакция: {transaction.id}\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'
'Баланс пополнен автоматически!'
)
await self.bot.send_message( await self.bot.send_message(
user.telegram_id, user.telegram_id,
@@ -404,23 +383,7 @@ class KassaAiPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
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:
from app.localization.texts import get_texts from app.localization.texts import get_texts
texts = get_texts(user.language) 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.config import settings
from app.database.models import PaymentMethod, TransactionType from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -390,28 +389,7 @@ class MulenPayPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
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
):
# Если у пользователя есть сохраненная корзина, # Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению # отправляем ему уведомление с кнопкой вернуться к оформлению
from app.localization.texts import get_texts 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.database.models import PaymentMethod, TransactionType
from app.services.pal24_service import Pal24APIError from app.services.pal24_service import Pal24APIError
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -489,23 +488,7 @@ class Pal24PaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
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:
from app.localization.texts import get_texts from app.localization.texts import get_texts
texts = get_texts(user.language) 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.database.models import PaymentMethod, TransactionType
from app.services.platega_service import PlategaService from app.services.platega_service import PlategaService
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -470,23 +469,7 @@ class PlategaPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
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:
from app.localization.texts import get_texts from app.localization.texts import get_texts
texts = get_texts(user.language) 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.database.models import PaymentMethod, TransactionType
from app.external.telegram_stars import TelegramStarsService from app.external.telegram_stars import TelegramStarsService
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -534,26 +533,7 @@ class TelegramStarsMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
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:
texts = get_texts(user.language) texts = get_texts(user.language)
cart_message = texts.t( cart_message = texts.t(
'BALANCE_TOPUP_CART_REMINDER_DETAILED', '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.config import settings
from app.database.models import PaymentMethod, TransactionType from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.services.wata_service import WataAPIError, WataService from app.services.wata_service import WataAPIError, WataService
@@ -575,23 +574,7 @@ class WataPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
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:
from app.localization.texts import get_texts from app.localization.texts import get_texts
texts = get_texts(user.language) 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.config import settings
from app.database.models import PaymentMethod, TransactionType from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.payment_logger import payment_logger as logger from app.utils.payment_logger import payment_logger as logger
@@ -847,78 +846,59 @@ class YooKassaPaymentMixin:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
if not auto_purchase_success: # Если у пользователя есть сохраненная корзина,
try: # отправляем ему уведомление с кнопкой вернуться к оформлению
await auto_activate_subscription_after_topup( from aiogram import types
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,
)
# Если включен яркий промпт активации, пропускаем старое уведомление from app.localization.texts import get_texts
# т.к. оно будет отправлено через _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 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( keyboard = types.InlineKeyboardMarkup(
total_amount=settings.format_price(payment.amount_kopeks) 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(
keyboard = types.InlineKeyboardMarkup( chat_id=user.telegram_id,
inline_keyboard=[ text=f'✅ Баланс пополнен на {settings.format_price(payment.amount_kopeks)}!\n\n'
[ f'⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. '
types.InlineKeyboardButton( f'Обязательно активируйте подписку отдельно!\n\n'
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT, f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
callback_data='return_to_saved_cart', f'подписка будет приобретена автоматически после пополнения баланса.\n\n{cart_message}',
) reply_markup=keyboard,
], )
[ logger.info(
types.InlineKeyboardButton( f'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю {user.id}'
text='💰 Мой баланс', )
callback_data='menu_balance', else:
) logger.info(
], 'У пользователя %s нет сохраненной корзины, бот недоступен или покупка уже выполнена',
[ user.id,
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,
)
except Exception as e: except Exception as e:
logger.error( logger.error(
f'Критическая ошибка при работе с сохраненной корзиной для пользователя {user.id}: {e}', f'Критическая ошибка при работе с сохраненной корзиной для пользователя {user.id}: {e}',
@@ -1814,340 +1814,4 @@ async def auto_purchase_saved_cart_after_topup(
return True return True
async def auto_activate_subscription_after_topup( __all__ = ['auto_purchase_saved_cart_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']
-14
View File
@@ -260,7 +260,6 @@ class BotConfigurationService:
'PAYMENT_BALANCE_TEMPLATE': 'PAYMENT', 'PAYMENT_BALANCE_TEMPLATE': 'PAYMENT',
'PAYMENT_SUBSCRIPTION_TEMPLATE': 'PAYMENT', 'PAYMENT_SUBSCRIPTION_TEMPLATE': 'PAYMENT',
'AUTO_PURCHASE_AFTER_TOPUP_ENABLED': 'PAYMENT', 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED': 'PAYMENT',
'SHOW_ACTIVATION_PROMPT_AFTER_TOPUP': 'PAYMENT',
'SIMPLE_SUBSCRIPTION_ENABLED': 'SIMPLE_SUBSCRIPTION', 'SIMPLE_SUBSCRIPTION_ENABLED': 'SIMPLE_SUBSCRIPTION',
'SIMPLE_SUBSCRIPTION_PERIOD_DAYS': 'SIMPLE_SUBSCRIPTION', 'SIMPLE_SUBSCRIPTION_PERIOD_DAYS': 'SIMPLE_SUBSCRIPTION',
'SIMPLE_SUBSCRIPTION_DEVICE_LIMIT': 'SIMPLE_SUBSCRIPTION', 'SIMPLE_SUBSCRIPTION_DEVICE_LIMIT': 'SIMPLE_SUBSCRIPTION',
@@ -585,19 +584,6 @@ class BotConfigurationService:
'example': 'true', 'example': 'true',
'warning': ('Используйте с осторожностью: средства будут списаны мгновенно, если корзина найдена.'), 'warning': ('Используйте с осторожностью: средства будут списаны мгновенно, если корзина найдена.'),
}, },
'SHOW_ACTIVATION_PROMPT_AFTER_TOPUP': {
'description': (
'Включает режим яркого промпта активации подписки после пополнения баланса. '
'Вместо обычного уведомления пользователь получит яркое сообщение с восклицательными знаками '
'и кнопками для активации/продления подписки или изменения количества устройств.'
),
'format': 'Булево значение.',
'example': 'true',
'warning': (
'При включении пользователи будут получать только яркое уведомление без кнопок баланса и главного меню. '
'Эти кнопки появятся после выполнения действия (активация/продление/изменение устройств).'
),
},
'SUPPORT_TICKET_SLA_MINUTES': { 'SUPPORT_TICKET_SLA_MINUTES': {
'description': 'Лимит времени для ответа модераторов на тикет в минутах.', 'description': 'Лимит времени для ответа модераторов на тикет в минутах.',
'format': 'Целое число от 1 до 1440.', '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.external.tribute import TributeService as TributeAPI
from app.services.payment_service import PaymentService from app.services.payment_service import PaymentService
from app.services.subscription_auto_purchase_service import ( from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup, auto_purchase_saved_cart_after_topup,
) )
from app.utils.user_utils import format_referrer_info from app.utils.user_utils import format_referrer_info
@@ -307,23 +306,8 @@ class TributeService:
if auto_purchase_success: if auto_purchase_success:
has_saved_cart = False has_saved_cart = False
# Умная автоактивация если автопокупка не сработала # Отправляем уведомление только если есть сохранённая корзина и telegram_id
activation_notification_sent = False if has_saved_cart and self.bot and user_id:
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:
# Если у пользователя есть сохраненная корзина, # Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению # отправляем ему уведомление с кнопкой вернуться к оформлению
from aiogram import types from aiogram import types
+10 -2
View File
@@ -2,7 +2,7 @@ import asyncio
import logging import logging
from aiogram import types from aiogram import types
from aiogram.exceptions import TelegramBadRequest, TelegramNetworkError from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramNetworkError
from aiogram.types import InaccessibleMessage, InputMediaPhoto from aiogram.types import InaccessibleMessage, InputMediaPhoto
from app.config import settings from app.config import settings
@@ -126,6 +126,8 @@ async def edit_or_answer_photo(
reply_markup=keyboard, reply_markup=keyboard,
parse_mode=resolved_parse_mode, parse_mode=resolved_parse_mode,
) )
except TelegramForbiddenError:
logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error: except TelegramBadRequest as error:
try: try:
await callback.message.delete() await callback.message.delete()
@@ -140,6 +142,8 @@ async def edit_or_answer_photo(
if callback.message.photo: if callback.message.photo:
await callback.message.delete() await callback.message.delete()
await _answer_text(callback, caption, keyboard, resolved_parse_mode) await _answer_text(callback, caption, keyboard, resolved_parse_mode)
except TelegramForbiddenError:
logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error: except TelegramBadRequest as error:
await _answer_text(callback, caption, keyboard, resolved_parse_mode, error) await _answer_text(callback, caption, keyboard, resolved_parse_mode, error)
return return
@@ -167,6 +171,10 @@ async def edit_or_answer_photo(
pass pass
await _answer_text(callback, caption, keyboard, resolved_parse_mode) await _answer_text(callback, caption, keyboard, resolved_parse_mode)
return return
except TelegramForbiddenError:
# Пользователь заблокировал бота — молча игнорируем
logger.debug('Пользователь заблокировал бота, пропускаем edit_media')
return
except TelegramBadRequest as error: except TelegramBadRequest as error:
if is_privacy_restricted_error(error): if is_privacy_restricted_error(error):
try: try:
@@ -189,7 +197,7 @@ async def edit_or_answer_photo(
parse_mode=resolved_parse_mode, parse_mode=resolved_parse_mode,
) )
_cache_logo_file_id(result) _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) await _answer_text(callback, caption, keyboard, resolved_parse_mode, photo_error)
except Exception: except Exception:
# Последний фоллбек — обычный текст # Последний фоллбек — обычный текст
@@ -5,71 +5,6 @@
from unittest.mock import MagicMock 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(): def test_telegram_id_saved_before_commit():
""" """
Тест: проверяем что telegram_id сохраняется в локальную переменную ДО commit. Тест: проверяем что telegram_id сохраняется в локальную переменную ДО commit.
Generated
+1 -1
View File
@@ -1149,7 +1149,7 @@ wheels = [
[[package]] [[package]]
name = "remnawave-bedolaga-telegram-bot" name = "remnawave-bedolaga-telegram-bot"
version = "3.6.0" version = "3.8.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiogram" }, { name = "aiogram" },