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:
+1
-7
@@ -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
|
||||||
|
|||||||
@@ -1342,7 +1342,6 @@ CONTEST_BUTTON_VISIBLE=true
|
|||||||
- 🔄 Автоплатёж с настройкой дня списания
|
- 🔄 Автоплатёж с настройкой дня списания
|
||||||
- 🎁 Реферальные и промо-бонусы
|
- 🎁 Реферальные и промо-бонусы
|
||||||
- ⚡ **Быстрое пополнение** с кнопками быстрых сумм
|
- ⚡ **Быстрое пополнение** с кнопками быстрых сумм
|
||||||
- 🔄 **Умная автоактивация** подписки после пополнения баланса
|
|
||||||
|
|
||||||
📱 **Управление подписками**
|
📱 **Управление подписками**
|
||||||
|
|
||||||
@@ -1529,7 +1528,6 @@ CONTEST_BUTTON_VISIBLE=true
|
|||||||
- 🔄 **Миграция сквадов** - массовый перенос пользователей между сквадами
|
- 🔄 **Миграция сквадов** - массовый перенос пользователей между сквадами
|
||||||
- 🧾 **История операций** - хранение всех транзакций и действий для аудита
|
- 🧾 **История операций** - хранение всех транзакций и действий для аудита
|
||||||
- 💸 **Сервис автопроверки транзакций** - автоматическая проверка транзакций в статусе "В ожидании оплаты" за последние 24ч
|
- 💸 **Сервис автопроверки транзакций** - автоматическая проверка транзакций в статусе "В ожидании оплаты" за последние 24ч
|
||||||
- 🔄 **Умная автоактивация** - автоматическая активация подписки после пополнения баланса
|
|
||||||
- 📝 **Ротация логов** - автоматическая очистка и архивация старых логов
|
- 📝 **Ротация логов** - автоматическая очистка и архивация старых логов
|
||||||
- 🎮 **Система конкурсов** - ежедневные игры и реферальные конкурсы с призами
|
- 🎮 **Система конкурсов** - ежедневные игры и реферальные конкурсы с призами
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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']
|
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user