From cdbeca245f03d1d9789d5b6b3fc1d7f9956b7c9f Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 23 Sep 2025 03:21:02 +0300 Subject: [PATCH 1/3] Add broadcast filters for expired and zero traffic subscriptions --- app/handlers/admin/messages.py | 114 +++++++++++++++++++++++++++------ app/keyboards/admin.py | 7 +- 2 files changed, 100 insertions(+), 21 deletions(-) diff --git a/app/handlers/admin/messages.py b/app/handlers/admin/messages.py index 83880515..49036553 100644 --- a/app/handlers/admin/messages.py +++ b/app/handlers/admin/messages.py @@ -2,6 +2,7 @@ import logging import asyncio from datetime import datetime, timedelta from typing import Optional + from aiogram import Dispatcher, types, F from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession @@ -260,14 +261,18 @@ async def select_broadcast_target( state: FSMContext, db: AsyncSession ): - target = callback.data.split('_')[-1] - + target = callback.data.replace("broadcast_", "", 1) + target_names = { "all": "Всем пользователям", "active": "С активной подпиской", - "trial": "С триальной подпиской", + "trial": "С триальной подпиской", "no": "Без подписки", - "expiring": "С истекающей подпиской" + "no_sub": "Без подписки", + "expiring": "С истекающей подпиской", + "expired": "С истекшей подпиской", + "active_zero_traffic": "Активная подписка без трафика", + "trial_zero_traffic": "Триальная подписка без трафика", } user_count = await get_target_users_count(db, target) @@ -817,22 +822,87 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int: async def get_target_users(db: AsyncSession, target: str) -> list: - if target == "all": - return await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - elif target == "active": - users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - return [user for user in users if user.subscription and user.subscription.is_active and not user.subscription.is_trial] - elif target == "trial": - users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - return [user for user in users if user.subscription and user.subscription.is_trial] - elif target == "no": - users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - return [user for user in users if not user.subscription or not user.subscription.is_active] - elif target == "expiring": - expiring_subs = await get_expiring_subscriptions(db, 3) - return [sub.user for sub in expiring_subs if sub.user] - else: - return [] + active_users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) + + if target == "all": + return active_users + if target == "active": + return [ + user + for user in active_users + if user.subscription + and user.subscription.is_active + and not user.subscription.is_trial + ] + if target == "trial": + return [ + user + for user in active_users + if user.subscription + and user.subscription.is_trial + and not user.subscription.is_expired + ] + if target in {"no", "no_sub"}: + return [ + user + for user in active_users + if not user.subscription + or ( + not user.subscription.is_active + and not ( + user.subscription.is_trial and not user.subscription.is_expired + ) + ) + ] + if target == "expiring": + expiring_subs = await get_expiring_subscriptions(db, 3) + active_user_ids = {user.id for user in active_users} + return [ + sub.user + for sub in expiring_subs + if sub.user and sub.user.id in active_user_ids + ] + if target == "expired": + return [ + user + for user in active_users + if user.subscription and user.subscription.is_expired + ] + if target == "active_zero_traffic": + return [ + user + for user in active_users + if user.subscription + and user.subscription.is_active + and not user.subscription.is_trial + and _subscription_has_zero_traffic(user.subscription) + ] + if target == "trial_zero_traffic": + return [ + user + for user in active_users + if user.subscription + and user.subscription.is_trial + and not user.subscription.is_expired + and _subscription_has_zero_traffic(user.subscription) + ] + + return [] + + +ZERO_TRAFFIC_EPSILON = 0.01 + + +def _subscription_has_zero_traffic(subscription: Optional[Subscription]) -> bool: + if not subscription: + return False + + limit = subscription.traffic_limit_gb or 0 + if limit == 0: + return False + + used = subscription.traffic_used_gb or 0.0 + return used >= float(limit) - ZERO_TRAFFIC_EPSILON async def get_custom_users_count(db: AsyncSession, criteria: str) -> int: @@ -956,7 +1026,11 @@ def get_target_name(target_type: str) -> str: "active": "С активной подпиской", "trial": "С триальной подпиской", "no": "Без подписки", + "no_sub": "Без подписки", "expiring": "С истекающей подпиской", + "expired": "С истекшей подпиской", + "active_zero_traffic": "Активная подписка без трафика", + "trial_zero_traffic": "Триальная подписка без трафика", "custom_today": "Зарегистрированные сегодня", "custom_week": "Зарегистрированные за неделю", "custom_month": "Зарегистрированные за месяц", diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index c7a3a480..3727b693 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -554,7 +554,12 @@ def get_broadcast_target_keyboard(language: str = "ru") -> InlineKeyboardMarkup: InlineKeyboardButton(text="❌ Без подписки", callback_data="broadcast_no_sub") ], [ - InlineKeyboardButton(text="⏰ Истекающие", callback_data="broadcast_expiring") + InlineKeyboardButton(text="⏰ Истекающие", callback_data="broadcast_expiring"), + InlineKeyboardButton(text="⛔️ Истекшие", callback_data="broadcast_expired") + ], + [ + InlineKeyboardButton(text="🛑 Активные (0 ГБ)", callback_data="broadcast_active_zero_traffic"), + InlineKeyboardButton(text="🛑 Триал (0 ГБ)", callback_data="broadcast_trial_zero_traffic") ], [ InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_messages") From badf679bc3b63a727eed7df1b2c401372eecb975 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 23 Sep 2025 03:23:12 +0300 Subject: [PATCH 2/3] Revert "Add broadcast filters for expired and zero traffic subscriptions" --- app/handlers/admin/messages.py | 114 ++++++--------------------------- app/keyboards/admin.py | 7 +- 2 files changed, 21 insertions(+), 100 deletions(-) diff --git a/app/handlers/admin/messages.py b/app/handlers/admin/messages.py index 49036553..83880515 100644 --- a/app/handlers/admin/messages.py +++ b/app/handlers/admin/messages.py @@ -2,7 +2,6 @@ import logging import asyncio from datetime import datetime, timedelta from typing import Optional - from aiogram import Dispatcher, types, F from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession @@ -261,18 +260,14 @@ async def select_broadcast_target( state: FSMContext, db: AsyncSession ): - target = callback.data.replace("broadcast_", "", 1) - + target = callback.data.split('_')[-1] + target_names = { "all": "Всем пользователям", "active": "С активной подпиской", - "trial": "С триальной подпиской", + "trial": "С триальной подпиской", "no": "Без подписки", - "no_sub": "Без подписки", - "expiring": "С истекающей подпиской", - "expired": "С истекшей подпиской", - "active_zero_traffic": "Активная подписка без трафика", - "trial_zero_traffic": "Триальная подписка без трафика", + "expiring": "С истекающей подпиской" } user_count = await get_target_users_count(db, target) @@ -822,87 +817,22 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int: async def get_target_users(db: AsyncSession, target: str) -> list: - active_users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - - if target == "all": - return active_users - if target == "active": - return [ - user - for user in active_users - if user.subscription - and user.subscription.is_active - and not user.subscription.is_trial - ] - if target == "trial": - return [ - user - for user in active_users - if user.subscription - and user.subscription.is_trial - and not user.subscription.is_expired - ] - if target in {"no", "no_sub"}: - return [ - user - for user in active_users - if not user.subscription - or ( - not user.subscription.is_active - and not ( - user.subscription.is_trial and not user.subscription.is_expired - ) - ) - ] - if target == "expiring": - expiring_subs = await get_expiring_subscriptions(db, 3) - active_user_ids = {user.id for user in active_users} - return [ - sub.user - for sub in expiring_subs - if sub.user and sub.user.id in active_user_ids - ] - if target == "expired": - return [ - user - for user in active_users - if user.subscription and user.subscription.is_expired - ] - if target == "active_zero_traffic": - return [ - user - for user in active_users - if user.subscription - and user.subscription.is_active - and not user.subscription.is_trial - and _subscription_has_zero_traffic(user.subscription) - ] - if target == "trial_zero_traffic": - return [ - user - for user in active_users - if user.subscription - and user.subscription.is_trial - and not user.subscription.is_expired - and _subscription_has_zero_traffic(user.subscription) - ] - - return [] - - -ZERO_TRAFFIC_EPSILON = 0.01 - - -def _subscription_has_zero_traffic(subscription: Optional[Subscription]) -> bool: - if not subscription: - return False - - limit = subscription.traffic_limit_gb or 0 - if limit == 0: - return False - - used = subscription.traffic_used_gb or 0.0 - return used >= float(limit) - ZERO_TRAFFIC_EPSILON + if target == "all": + return await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) + elif target == "active": + users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) + return [user for user in users if user.subscription and user.subscription.is_active and not user.subscription.is_trial] + elif target == "trial": + users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) + return [user for user in users if user.subscription and user.subscription.is_trial] + elif target == "no": + users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) + return [user for user in users if not user.subscription or not user.subscription.is_active] + elif target == "expiring": + expiring_subs = await get_expiring_subscriptions(db, 3) + return [sub.user for sub in expiring_subs if sub.user] + else: + return [] async def get_custom_users_count(db: AsyncSession, criteria: str) -> int: @@ -1026,11 +956,7 @@ def get_target_name(target_type: str) -> str: "active": "С активной подпиской", "trial": "С триальной подпиской", "no": "Без подписки", - "no_sub": "Без подписки", "expiring": "С истекающей подпиской", - "expired": "С истекшей подпиской", - "active_zero_traffic": "Активная подписка без трафика", - "trial_zero_traffic": "Триальная подписка без трафика", "custom_today": "Зарегистрированные сегодня", "custom_week": "Зарегистрированные за неделю", "custom_month": "Зарегистрированные за месяц", diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index 3727b693..c7a3a480 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -554,12 +554,7 @@ def get_broadcast_target_keyboard(language: str = "ru") -> InlineKeyboardMarkup: InlineKeyboardButton(text="❌ Без подписки", callback_data="broadcast_no_sub") ], [ - InlineKeyboardButton(text="⏰ Истекающие", callback_data="broadcast_expiring"), - InlineKeyboardButton(text="⛔️ Истекшие", callback_data="broadcast_expired") - ], - [ - InlineKeyboardButton(text="🛑 Активные (0 ГБ)", callback_data="broadcast_active_zero_traffic"), - InlineKeyboardButton(text="🛑 Триал (0 ГБ)", callback_data="broadcast_trial_zero_traffic") + InlineKeyboardButton(text="⏰ Истекающие", callback_data="broadcast_expiring") ], [ InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_messages") From d6fb26d425c9abfb600ccea5027e5c989970101f Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 23 Sep 2025 03:24:28 +0300 Subject: [PATCH 3/3] Add extended broadcast filters for subscription states --- app/handlers/admin/messages.py | 107 +++++++++++++++++++++++++++------ app/keyboards/admin.py | 7 ++- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/app/handlers/admin/messages.py b/app/handlers/admin/messages.py index 83880515..6f6eef66 100644 --- a/app/handlers/admin/messages.py +++ b/app/handlers/admin/messages.py @@ -9,7 +9,13 @@ from sqlalchemy import select, func, and_, or_ from app.config import settings from app.states import AdminStates -from app.database.models import User, UserStatus, Subscription, BroadcastHistory +from app.database.models import ( + User, + UserStatus, + Subscription, + SubscriptionStatus, + BroadcastHistory, +) from app.keyboards.admin import ( get_admin_messages_keyboard, get_broadcast_target_keyboard, get_custom_criteria_keyboard, get_broadcast_history_keyboard, @@ -265,9 +271,12 @@ async def select_broadcast_target( target_names = { "all": "Всем пользователям", "active": "С активной подпиской", - "trial": "С триальной подпиской", + "trial": "С триальной подпиской", "no": "Без подписки", - "expiring": "С истекающей подпиской" + "expiring": "С истекающей подпиской", + "expired": "С истекшей подпиской", + "active_zero": "Активная подписка, трафик 0 ГБ", + "trial_zero": "Триальная подписка, трафик 0 ГБ", } user_count = await get_target_users_count(db, target) @@ -817,22 +826,79 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int: async def get_target_users(db: AsyncSession, target: str) -> list: - if target == "all": - return await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - elif target == "active": - users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - return [user for user in users if user.subscription and user.subscription.is_active and not user.subscription.is_trial] - elif target == "trial": - users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - return [user for user in users if user.subscription and user.subscription.is_trial] - elif target == "no": - users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) - return [user for user in users if not user.subscription or not user.subscription.is_active] - elif target == "expiring": - expiring_subs = await get_expiring_subscriptions(db, 3) - return [sub.user for sub in expiring_subs if sub.user] - else: - return [] + users = await get_users_list(db, offset=0, limit=10000, status=UserStatus.ACTIVE) + + if target == "all": + return users + + if target == "active": + return [ + user + for user in users + if user.subscription + and user.subscription.is_active + and not user.subscription.is_trial + ] + + if target == "trial": + return [ + user + for user in users + if user.subscription and user.subscription.is_trial + ] + + if target == "no": + return [ + user + for user in users + if not user.subscription or not user.subscription.is_active + ] + + if target == "expiring": + expiring_subs = await get_expiring_subscriptions(db, 3) + return [sub.user for sub in expiring_subs if sub.user] + + if target == "expired": + now = datetime.utcnow() + expired_statuses = { + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.DISABLED.value, + } + expired_users = [] + for user in users: + subscription = user.subscription + if subscription: + if subscription.status in expired_statuses: + expired_users.append(user) + continue + if subscription.end_date <= now and not subscription.is_active: + expired_users.append(user) + continue + elif user.has_had_paid_subscription: + expired_users.append(user) + return expired_users + + if target == "active_zero": + return [ + user + for user in users + if user.subscription + and not user.subscription.is_trial + and user.subscription.is_active + and (user.subscription.traffic_used_gb or 0) <= 0 + ] + + if target == "trial_zero": + return [ + user + for user in users + if user.subscription + and user.subscription.is_trial + and user.subscription.is_active + and (user.subscription.traffic_used_gb or 0) <= 0 + ] + + return [] async def get_custom_users_count(db: AsyncSession, criteria: str) -> int: @@ -957,6 +1023,9 @@ def get_target_name(target_type: str) -> str: "trial": "С триальной подпиской", "no": "Без подписки", "expiring": "С истекающей подпиской", + "expired": "С истекшей подпиской", + "active_zero": "Активная подписка, трафик 0 ГБ", + "trial_zero": "Триальная подписка, трафик 0 ГБ", "custom_today": "Зарегистрированные сегодня", "custom_week": "Зарегистрированные за неделю", "custom_month": "Зарегистрированные за месяц", diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index c7a3a480..df1db350 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -554,7 +554,12 @@ def get_broadcast_target_keyboard(language: str = "ru") -> InlineKeyboardMarkup: InlineKeyboardButton(text="❌ Без подписки", callback_data="broadcast_no_sub") ], [ - InlineKeyboardButton(text="⏰ Истекающие", callback_data="broadcast_expiring") + InlineKeyboardButton(text="⏰ Истекающие", callback_data="broadcast_expiring"), + InlineKeyboardButton(text="🔚 Истекшие", callback_data="broadcast_expired") + ], + [ + InlineKeyboardButton(text="🧊 Активна 0 ГБ", callback_data="broadcast_active_zero"), + InlineKeyboardButton(text="🥶 Триал 0 ГБ", callback_data="broadcast_trial_zero") ], [ InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_messages")