mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Fix trial reset by clearing server links
This commit is contained in:
@@ -59,6 +59,7 @@ from app.handlers.admin import (
|
||||
public_offer as admin_public_offer,
|
||||
faq as admin_faq,
|
||||
payments as admin_payments,
|
||||
trials as admin_trials,
|
||||
)
|
||||
from app.handlers.stars_payments import register_stars_handlers
|
||||
|
||||
@@ -174,6 +175,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_public_offer.register_handlers(dp)
|
||||
admin_faq.register_handlers(dp)
|
||||
admin_payments.register_handlers(dp)
|
||||
admin_trials.register_handlers(dp)
|
||||
common.register_handlers(dp)
|
||||
register_stars_handlers(dp)
|
||||
user_polls.register_handlers(dp)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Iterable, Optional, List, Tuple
|
||||
from sqlalchemy import select, and_, func
|
||||
from sqlalchemy import select, and_, func, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -676,10 +676,113 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
|
||||
"purchased_today": purchased_today,
|
||||
"purchased_week": purchased_week,
|
||||
"purchased_month": purchased_month,
|
||||
"trial_to_paid_conversion": trial_to_paid_conversion,
|
||||
"renewals_count": renewals_count
|
||||
"trial_to_paid_conversion": trial_to_paid_conversion,
|
||||
"renewals_count": renewals_count
|
||||
}
|
||||
|
||||
|
||||
async def get_trial_statistics(db: AsyncSession) -> dict:
|
||||
now = datetime.utcnow()
|
||||
|
||||
total_trials_result = await db.execute(
|
||||
select(func.count(Subscription.id)).where(Subscription.is_trial.is_(True))
|
||||
)
|
||||
total_trials = total_trials_result.scalar() or 0
|
||||
|
||||
active_trials_result = await db.execute(
|
||||
select(func.count(Subscription.id)).where(
|
||||
Subscription.is_trial.is_(True),
|
||||
Subscription.end_date > now,
|
||||
Subscription.status.in_(
|
||||
[SubscriptionStatus.TRIAL.value, SubscriptionStatus.ACTIVE.value]
|
||||
),
|
||||
)
|
||||
)
|
||||
active_trials = active_trials_result.scalar() or 0
|
||||
|
||||
resettable_trials_result = await db.execute(
|
||||
select(func.count(Subscription.id))
|
||||
.join(User, Subscription.user_id == User.id)
|
||||
.where(
|
||||
Subscription.is_trial.is_(True),
|
||||
Subscription.end_date <= now,
|
||||
User.has_had_paid_subscription.is_(False),
|
||||
)
|
||||
)
|
||||
resettable_trials = resettable_trials_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"used_trials": total_trials,
|
||||
"active_trials": active_trials,
|
||||
"resettable_trials": resettable_trials,
|
||||
}
|
||||
|
||||
|
||||
async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) -> int:
|
||||
now = datetime.utcnow()
|
||||
|
||||
result = await db.execute(
|
||||
select(Subscription)
|
||||
.options(
|
||||
selectinload(Subscription.user),
|
||||
selectinload(Subscription.subscription_servers),
|
||||
)
|
||||
.join(User, Subscription.user_id == User.id)
|
||||
.where(
|
||||
Subscription.is_trial.is_(True),
|
||||
Subscription.end_date <= now,
|
||||
User.has_had_paid_subscription.is_(False),
|
||||
)
|
||||
)
|
||||
|
||||
subscriptions = result.scalars().unique().all()
|
||||
if not subscriptions:
|
||||
return 0
|
||||
|
||||
reset_count = len(subscriptions)
|
||||
for subscription in subscriptions:
|
||||
try:
|
||||
await decrement_subscription_server_counts(
|
||||
db,
|
||||
subscription,
|
||||
subscription_servers=subscription.subscription_servers,
|
||||
)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error(
|
||||
"Не удалось обновить счётчики серверов при сбросе триала %s: %s",
|
||||
subscription.id,
|
||||
error,
|
||||
)
|
||||
|
||||
subscription_ids = [subscription.id for subscription in subscriptions]
|
||||
|
||||
if subscription_ids:
|
||||
try:
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id.in_(subscription_ids)
|
||||
)
|
||||
)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error(
|
||||
"Ошибка удаления серверных связей триалов %s: %s",
|
||||
subscription_ids,
|
||||
error,
|
||||
)
|
||||
raise
|
||||
|
||||
await db.execute(delete(Subscription).where(Subscription.id.in_(subscription_ids)))
|
||||
|
||||
try:
|
||||
await db.commit()
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
await db.rollback()
|
||||
logger.error("Ошибка сохранения сброса триалов: %s", error)
|
||||
raise
|
||||
|
||||
logger.info("♻️ Сброшено триальных подписок: %s", reset_count)
|
||||
return reset_count
|
||||
|
||||
async def update_subscription_usage(
|
||||
db: AsyncSession,
|
||||
subscription: Subscription,
|
||||
|
||||
86
app/handlers/admin/trials.py
Normal file
86
app/handlers/admin/trials.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
|
||||
from aiogram import Dispatcher, F, types
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.crud.subscription import (
|
||||
get_trial_statistics,
|
||||
reset_trials_for_users_without_paid_subscription,
|
||||
)
|
||||
from app.database.models import User
|
||||
from app.keyboards.admin import get_admin_trials_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_trials_panel(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
stats = await get_trial_statistics(db)
|
||||
message = texts.t("ADMIN_TRIALS_TITLE", "🧪 Управление триалами") + "\n\n" + texts.t(
|
||||
"ADMIN_TRIALS_STATS",
|
||||
"• Использовано всего: {used}\n"
|
||||
"• Активно сейчас: {active}\n"
|
||||
"• Доступно к сбросу: {resettable}",
|
||||
).format(
|
||||
used=stats.get("used_trials", 0),
|
||||
active=stats.get("active_trials", 0),
|
||||
resettable=stats.get("resettable_trials", 0),
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=get_admin_trials_keyboard(db_user.language),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def reset_trials(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
reset_count = await reset_trials_for_users_without_paid_subscription(db)
|
||||
stats = await get_trial_statistics(db)
|
||||
|
||||
message = texts.t(
|
||||
"ADMIN_TRIALS_RESET_RESULT",
|
||||
"♻️ Сбросили {reset_count} триалов.\n\n"
|
||||
"• Использовано всего: {used}\n"
|
||||
"• Активно сейчас: {active}\n"
|
||||
"• Доступно к сбросу: {resettable}",
|
||||
).format(
|
||||
reset_count=reset_count,
|
||||
used=stats.get("used_trials", 0),
|
||||
active=stats.get("active_trials", 0),
|
||||
resettable=stats.get("resettable_trials", 0),
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=get_admin_trials_keyboard(db_user.language),
|
||||
)
|
||||
await callback.answer(texts.t("ADMIN_TRIALS_RESET_TOAST", "✅ Сброс завершен"))
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher) -> None:
|
||||
dp.callback_query.register(
|
||||
show_trials_panel,
|
||||
F.data == "admin_trials",
|
||||
)
|
||||
dp.callback_query.register(
|
||||
reset_trials,
|
||||
F.data == "admin_trials_reset",
|
||||
)
|
||||
@@ -54,6 +54,10 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_MAIN_TRIALS", "🧪 Триалы"),
|
||||
callback_data="admin_trials",
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_MAIN_PAYMENTS", "💳 Пополнения"),
|
||||
callback_data="admin_payments",
|
||||
@@ -241,6 +245,20 @@ def get_admin_system_submenu_keyboard(language: str = "ru") -> InlineKeyboardMar
|
||||
])
|
||||
|
||||
|
||||
def get_admin_trials_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_TRIALS_RESET_BUTTON", "♻️ Сбросить все триалы"),
|
||||
callback_data="admin_trials_reset",
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")],
|
||||
])
|
||||
|
||||
|
||||
def get_admin_reports_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"ADMIN_MAIN_SETTINGS": "⚙️ Settings",
|
||||
"ADMIN_MAIN_SUPPORT": "🛟 Support",
|
||||
"ADMIN_MAIN_SYSTEM": "🛠️ System",
|
||||
"ADMIN_MAIN_TRIALS": "🧪 Trials",
|
||||
"ADMIN_MAIN_PAYMENTS": "💳 Top-ups",
|
||||
"ADMIN_MAIN_USERS_SUBSCRIPTIONS": "👥 Users / Subscriptions",
|
||||
"ADMIN_MESSAGES": "📨 Broadcasts",
|
||||
@@ -168,6 +169,11 @@
|
||||
"ADMIN_PAYMENTS_TITLE": "💳 <b>Top-up verification</b>",
|
||||
"ADMIN_PAYMENTS_DESCRIPTION": "Pending top-up invoices created during the last 24 hours.",
|
||||
"ADMIN_PAYMENTS_NOTICE": "Only invoices younger than 24 hours and waiting for payment can be checked.",
|
||||
"ADMIN_TRIALS_TITLE": "🧪 Trial management",
|
||||
"ADMIN_TRIALS_STATS": "• Total trials used: {used}\n• Active now: {active}\n• Eligible for reset: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_BUTTON": "♻️ Reset all trials",
|
||||
"ADMIN_TRIALS_RESET_RESULT": "♻️ Reset {reset_count} trials.\n\n• Total trials used: {used}\n• Active now: {active}\n• Eligible for reset: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_TOAST": "✅ Reset completed",
|
||||
"ADMIN_PAYMENTS_EMPTY": "No pending top-up invoices found in the last 24 hours.",
|
||||
"ADMIN_PAYMENTS_ITEM_DETAILS": "📄 #{number}",
|
||||
"ADMIN_PAYMENT_STATUS_PENDING": "Pending",
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"ADMIN_MAIN_SETTINGS": "⚙️ Настройки",
|
||||
"ADMIN_MAIN_SUPPORT": "🛟 Поддержка",
|
||||
"ADMIN_MAIN_SYSTEM": "🛠️ Система",
|
||||
"ADMIN_MAIN_TRIALS": "🧪 Триалы",
|
||||
"ADMIN_MAIN_PAYMENTS": "💳 Пополнения",
|
||||
"ADMIN_MAIN_USERS_SUBSCRIPTIONS": "👥 Юзеры/Подписки",
|
||||
"ADMIN_MESSAGES": "📨 Рассылки",
|
||||
@@ -168,6 +169,11 @@
|
||||
"ADMIN_PAYMENTS_TITLE": "💳 <b>Проверка пополнений</b>",
|
||||
"ADMIN_PAYMENTS_DESCRIPTION": "Список счетов на пополнение, созданных за последние 24 часа и ожидающих оплаты.",
|
||||
"ADMIN_PAYMENTS_NOTICE": "Проверять можно только счета моложе 24 часов и со статусом ожидания.",
|
||||
"ADMIN_TRIALS_TITLE": "🧪 Управление триалами",
|
||||
"ADMIN_TRIALS_STATS": "• Использовано всего: {used}\n• Активно сейчас: {active}\n• Доступно к сбросу: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_BUTTON": "♻️ Сбросить все триалы",
|
||||
"ADMIN_TRIALS_RESET_RESULT": "♻️ Сбросили {reset_count} триалов.\n\n• Использовано всего: {used}\n• Активно сейчас: {active}\n• Доступно к сбросу: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_TOAST": "✅ Сброс завершен",
|
||||
"ADMIN_PAYMENTS_EMPTY": "За последние 24 часа не найдено счетов на пополнение в ожидании.",
|
||||
"ADMIN_PAYMENTS_ITEM_DETAILS": "📄 №{number}",
|
||||
"ADMIN_PAYMENT_STATUS_PENDING": "Ожидает оплаты",
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"ADMIN_MAIN_SETTINGS": "⚙️ Налаштування",
|
||||
"ADMIN_MAIN_SUPPORT": "🛟 Підтримка",
|
||||
"ADMIN_MAIN_SYSTEM": "🛠️ Система",
|
||||
"ADMIN_MAIN_TRIALS": "🧪 Тріали",
|
||||
"ADMIN_MAIN_PAYMENTS": "💳 Поповнення",
|
||||
"ADMIN_MAIN_USERS_SUBSCRIPTIONS": "👥 Юзери/Підписки",
|
||||
"ADMIN_MESSAGES": "📨 Розсилки",
|
||||
@@ -167,6 +168,11 @@
|
||||
"ADMIN_PAYMENTS_TITLE": "💳 <b>Перевірка поповнень</b>",
|
||||
"ADMIN_PAYMENTS_DESCRIPTION": "Список рахунків на поповнення, створених за останні 24 години, які очікують на оплату.",
|
||||
"ADMIN_PAYMENTS_NOTICE": "Перевіряти можна лише рахунки, молодші 24 годин, зі статусом очікування.",
|
||||
"ADMIN_TRIALS_TITLE": "🧪 Керування тріалами",
|
||||
"ADMIN_TRIALS_STATS": "• Використано всього: {used}\n• Активні зараз: {active}\n• Доступні для скидання: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_BUTTON": "♻️ Скинути всі тріали",
|
||||
"ADMIN_TRIALS_RESET_RESULT": "♻️ Скинули {reset_count} тріалів.\n\n• Використано всього: {used}\n• Активні зараз: {active}\n• Доступні для скидання: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_TOAST": "✅ Скидання завершено",
|
||||
"ADMIN_PAYMENTS_EMPTY": "За останні 24 години не знайдено рахунків на поповнення в очікуванні.",
|
||||
"ADMIN_PAYMENTS_ITEM_DETAILS": "📄 №{number}",
|
||||
"ADMIN_PAYMENT_STATUS_PENDING": "Очікує оплати",
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"ADMIN_MAIN_SETTINGS":"⚙️设置",
|
||||
"ADMIN_MAIN_SUPPORT":"🛟支持",
|
||||
"ADMIN_MAIN_SYSTEM":"🛠️系统",
|
||||
"ADMIN_MAIN_TRIALS":"🧪试用",
|
||||
"ADMIN_MAIN_PAYMENTS":"💳充值",
|
||||
"ADMIN_MAIN_USERS_SUBSCRIPTIONS":"👥用户/订阅",
|
||||
"ADMIN_MESSAGES":"📨广播",
|
||||
@@ -167,6 +168,11 @@
|
||||
"ADMIN_PAYMENTS_TITLE":"💳<b>充值检查</b>",
|
||||
"ADMIN_PAYMENTS_DESCRIPTION":"过去24小时内创建并等待付款的充值账单列表。",
|
||||
"ADMIN_PAYMENTS_NOTICE":"只能检查24小时内且状态为等待中的账单。",
|
||||
"ADMIN_TRIALS_TITLE":"🧪 试用管理",
|
||||
"ADMIN_TRIALS_STATS":"• 已使用试用总数: {used}\n• 当前活跃: {active}\n• 可重置: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_BUTTON":"♻️ 重置所有试用",
|
||||
"ADMIN_TRIALS_RESET_RESULT":"♻️ 已重置 {reset_count} 个试用。\n\n• 已使用试用总数: {used}\n• 当前活跃: {active}\n• 可重置: {resettable}",
|
||||
"ADMIN_TRIALS_RESET_TOAST":"✅ 重置完成",
|
||||
"ADMIN_PAYMENTS_EMPTY":"过去24小时内未找到等待中的充值账单。",
|
||||
"ADMIN_PAYMENTS_ITEM_DETAILS":"📄№{number}",
|
||||
"ADMIN_PAYMENT_STATUS_PENDING":"等待付款",
|
||||
|
||||
Reference in New Issue
Block a user