diff --git a/api/routes/misc.py b/api/routes/misc.py index 61de5822..456e9306 100644 --- a/api/routes/misc.py +++ b/api/routes/misc.py @@ -163,7 +163,9 @@ async def get_tracking_source_with_stats( type=source.type, created_by=source.created_by, created_at=source.created_at, - registrations=stats.get("registrations", 0), - trials=stats.get("trials", 0), - payments=stats.get("payments", 0), + registrations=(stats["registrations"] if stats else 0), + trials=(stats["trials"] if stats else 0), + payments=(stats["payments"] if stats else 0), + total_amount=(float(stats["total_amount"]) if stats else 0.0), + monthly=(stats["monthly"] if stats and "monthly" in stats else []), ) diff --git a/api/schemas/misc.py b/api/schemas/misc.py index d97bed65..3537d917 100644 --- a/api/schemas/misc.py +++ b/api/schemas/misc.py @@ -94,6 +94,16 @@ class BlockedUserResponse(BaseModel): from_attributes = True +class MonthlyStats(BaseModel): + month: str + registrations: int + trials: int + new_purchases_count: int + new_purchases_amount: float + repeat_purchases_count: int + repeat_purchases_amount: float + + class TrackingSourceResponse(BaseModel): id: int name: str @@ -105,6 +115,9 @@ class TrackingSourceResponse(BaseModel): registrations: int = 0 trials: int = 0 payments: int = 0 + total_amount: float = 0.0 + + monthly: list[MonthlyStats] = [] class Config: from_attributes = True diff --git a/api/schemas/tariffs.py b/api/schemas/tariffs.py index e1585958..f4f98690 100644 --- a/api/schemas/tariffs.py +++ b/api/schemas/tariffs.py @@ -1,5 +1,4 @@ from datetime import datetime - from pydantic import BaseModel @@ -12,6 +11,7 @@ class TariffBase(BaseModel): device_limit: int | None = None is_active: bool = True subgroup_title: str | None = None + sort_order: int | None = None class TariffResponse(TariffBase): @@ -32,6 +32,7 @@ class TariffUpdate(BaseModel): device_limit: int | None = None is_active: bool | None = None subgroup_title: str | None = None + sort_order: int | None = None class Config: from_attributes = True diff --git a/bot.py b/bot.py index 11994a8c..790fba80 100644 --- a/bot.py +++ b/bot.py @@ -13,6 +13,7 @@ from config import ADMIN_ID, API_TOKEN from filters.private import IsPrivateFilter from logger import logger from utils.modules_loader import load_modules_from_folder +from database import async_session_maker bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) @@ -52,26 +53,28 @@ async def errors_handler(event: ErrorEvent, bot: Bot) -> bool: chat_id=event.update.message.chat.id, user_id=event.update.message.from_user.id, ) - await start_entry( - event=event.update.message, - state=fsm_context, - session=None, - admin=False, - captcha=False, - ) + async with async_session_maker() as session: + await start_entry( + event=event.update.message, + state=fsm_context, + session=session, + admin=False, + captcha=False, + ) elif event.update.callback_query: fsm_context = dp.fsm.get_context( bot=bot, chat_id=event.update.callback_query.message.chat.id, user_id=event.update.callback_query.from_user.id, ) - await start_entry( - event=event.update.callback_query, - state=fsm_context, - session=None, - admin=False, - captcha=False, - ) + async with async_session_maker() as session: + await start_entry( + event=event.update.callback_query, + state=fsm_context, + session=session, + admin=False, + captcha=False, + ) except Exception as e: logger.error(f"Ошибка при показе стартового меню после ошибки: {e}", exc_info=True) @@ -93,38 +96,37 @@ async def errors_handler(event: ErrorEvent, bot: Bot) -> bool: caption=f"{hbold(type(event.exception).__name__)}: {str(event.exception)[:1021]}...", ) - from handlers.start import start_entry - if event.update.message: fsm_context = dp.fsm.get_context( bot=bot, chat_id=event.update.message.chat.id, user_id=event.update.message.from_user.id, ) - await start_entry( - event=event.update.message, - state=fsm_context, - session=None, - admin=False, - captcha=False, - ) + async with async_session_maker() as session: + await start_entry( + event=event.update.message, + state=fsm_context, + session=session, + admin=False, + captcha=False, + ) elif event.update.callback_query: fsm_context = dp.fsm.get_context( bot=bot, chat_id=event.update.callback_query.message.chat.id, user_id=event.update.callback_query.from_user.id, ) - await start_entry( - event=event.update.callback_query, - state=fsm_context, - session=None, - admin=False, - captcha=False, - ) + async with async_session_maker() as session: + await start_entry( + event=event.update.callback_query, + state=fsm_context, + session=session, + admin=False, + captcha=False, + ) except TelegramBadRequest as exception: logger.warning(f"Не удалось отправить детали ошибки: {exception}") except Exception as exception: logger.error(f"Неожиданная ошибка в error handler: {exception}") - return True diff --git a/database/tracking_sources.py b/database/tracking_sources.py index 910ff45b..fc6b9e4d 100644 --- a/database/tracking_sources.py +++ b/database/tracking_sources.py @@ -1,4 +1,4 @@ -from sqlalchemy import func, insert, not_, select +from sqlalchemy import func, insert, not_, select, and_ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession @@ -6,6 +6,8 @@ from database.models import Payment, TrackingSource, User from logger import logger +EXCLUDED_PAYMENT_MARKERS = ["coupon", "referral", "cashback"] + async def create_tracking_source(session: AsyncSession, name: str, code: str, type_: str, created_by: int): try: stmt = insert(TrackingSource).values( @@ -70,12 +72,18 @@ async def get_all_tracking_sources(session: AsyncSession) -> list[dict]: async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | None: - source_result = await session.execute(select(TrackingSource.created_at).where(TrackingSource.code == code)) - created_at_row = source_result.first() - if not created_at_row: + def _month_key(dt) -> str: + return dt.strftime("%Y-%m") + + src_row = await session.execute( + select(TrackingSource.name, TrackingSource.code, TrackingSource.created_at) + .where(TrackingSource.code == code) + ) + src = src_row.first() + if not src: return None - created_at = created_at_row[0] + src_name, src_code, created_at = src reg_subq = ( select(func.count(func.distinct(User.tg_id))) @@ -95,25 +103,25 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | .where( (User.source_code == code) & (Payment.status == "success") - & not_(Payment.payment_system.in_(["coupon", "referral", "cashback"])) + & not_(Payment.payment_system.in_(EXCLUDED_PAYMENT_MARKERS)) & (Payment.created_at >= created_at) ) .scalar_subquery() ) amount_subq = ( - select(func.coalesce(func.sum(Payment.amount), 0)) + select(func.coalesce(func.sum(Payment.amount), 0.0)) .join(User, Payment.tg_id == User.tg_id) .where( (User.source_code == code) & (Payment.status == "success") - & not_(Payment.payment_system.in_(["coupon", "referral", "cashback"])) + & not_(Payment.payment_system.in_(EXCLUDED_PAYMENT_MARKERS)) & (Payment.created_at >= created_at) ) .scalar_subquery() ) - query = select( + header_q = select( TrackingSource.name, TrackingSource.code, TrackingSource.created_at, @@ -123,11 +131,119 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | amount_subq.label("total_amount"), ).where(TrackingSource.code == code) - result = await session.execute(query) - row = result.first() + header_res = await session.execute(header_q) + row = header_res.first() if not row: return None + payments_base = ( + select( + Payment.tg_id.label("tg_id"), + Payment.amount.label("amount"), + Payment.created_at.label("dt"), + ) + .join(User, Payment.tg_id == User.tg_id) + .where( + (User.source_code == code) + & (Payment.status == "success") + & not_(Payment.payment_system.in_(EXCLUDED_PAYMENT_MARKERS)) + & (Payment.created_at >= created_at) + ) + .subquery() + ) + + first_pay = ( + select( + payments_base.c.tg_id.label("tg_id"), + func.min(payments_base.c.dt).label("first_dt"), + ) + .group_by(payments_base.c.tg_id) + .subquery() + ) + + month_expr_new = func.date_trunc("month", payments_base.c.dt).label("month") + new_rows = await session.execute( + select( + month_expr_new, + func.count().label("cnt"), + func.coalesce(func.sum(payments_base.c.amount), 0.0).label("amt"), + ) + .join( + first_pay, + and_( + payments_base.c.tg_id == first_pay.c.tg_id, + payments_base.c.dt == first_pay.c.first_dt, + ), + ) + .group_by(month_expr_new) + .order_by(month_expr_new) + ) + new_by_month = {r.month: (int(r.cnt), float(r.amt)) for r in new_rows.all()} + + month_expr_rep = func.date_trunc("month", payments_base.c.dt).label("month") + repeat_rows = await session.execute( + select( + month_expr_rep, + func.count().label("cnt"), + func.coalesce(func.sum(payments_base.c.amount), 0.0).label("amt"), + ) + .join(first_pay, payments_base.c.tg_id == first_pay.c.tg_id) + .where(payments_base.c.dt > first_pay.c.first_dt) + .group_by(month_expr_rep) + .order_by(month_expr_rep) + ) + repeat_by_month = {r.month: (int(r.cnt), float(r.amt)) for r in repeat_rows.all()} + + month_expr_regs = func.date_trunc("month", User.created_at).label("month") + regs_rows = await session.execute( + select( + month_expr_regs, + func.count(func.distinct(User.tg_id)).label("cnt"), + ) + .where((User.source_code == code) & (User.created_at >= created_at)) + .group_by(month_expr_regs) + .order_by(month_expr_regs) + ) + regs_by_month = {r.month: int(r.cnt) for r in regs_rows.all()} + + month_expr_trials = func.date_trunc("month", User.created_at).label("month") + trials_rows = await session.execute( + select( + month_expr_trials, + func.count(func.distinct(User.tg_id)).label("cnt"), + ) + .where( + (User.source_code == code) + & (User.trial == 1) + & (User.created_at >= created_at) + ) + .group_by(month_expr_trials) + .order_by(month_expr_trials) + ) + trials_by_month = {r.month: int(r.cnt) for r in trials_rows.all()} + + months = set() + months.update(regs_by_month.keys()) + months.update(trials_by_month.keys()) + months.update(new_by_month.keys()) + months.update(repeat_by_month.keys()) + + monthly = [] + for m in sorted(months): + regs = regs_by_month.get(m, 0) + trls = trials_by_month.get(m, 0) + new_cnt, new_amt = new_by_month.get(m, (0, 0.0)) + rep_cnt, rep_amt = repeat_by_month.get(m, (0, 0.0)) + monthly.append({ + "month": _month_key(m), + "registrations": regs, + "trials": trls, + "new_purchases_count": new_cnt, + "new_purchases_amount": new_amt, + "repeat_purchases_count": rep_cnt, + "repeat_purchases_amount": rep_amt, + }) + return { "name": row.name, "code": row.code, @@ -136,4 +252,5 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | "trials": row.trials or 0, "payments": row.payments or 0, "total_amount": float(row.total_amount or 0), + "monthly": monthly, } diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index 81a4da49..6c8d5366 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -12,7 +12,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMar from sqlalchemy import distinct, func, select from sqlalchemy.ext.asyncio import AsyncSession -from database.models import Key, Payment, Server, Tariff, User +from database.models import Key, Payment, Server, Tariff, User, BlockedUser, ManualBan from filters.admin import IsAdminFilter from logger import logger @@ -234,9 +234,17 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, except Exception as e: logger.error(f"Ошибка восстановления клавиатуры: {e}") + banned_tg_ids = ( + select(BlockedUser.tg_id).union_all( + select(ManualBan.tg_id).where( + (ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow()) + ) + ) + ) + query = None if send_to == "subscribed": - query = select(distinct(User.tg_id)).join(Key).where(Key.expiry_time > now_ms) + query = select(distinct(User.tg_id)).join(Key).where(Key.expiry_time > now_ms).where(~User.tg_id.in_(banned_tg_ids)) elif send_to == "unsubscribed": subquery = ( select(User.tg_id) @@ -250,16 +258,17 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, .having(func.max(Key.expiry_time) <= now_ms) ) ) - query = select(distinct(subquery.c.tg_id)) + query = select(distinct(subquery.c.tg_id)).where(~subquery.c.tg_id.in_(banned_tg_ids)) elif send_to == "untrial": subquery = select(Key.tg_id) - query = select(distinct(User.tg_id)).where(~User.tg_id.in_(subquery) & User.trial.in_([0, -1])) + query = select(distinct(User.tg_id)).where(~User.tg_id.in_(subquery) & User.trial.in_([0, -1])).where(~User.tg_id.in_(banned_tg_ids)) elif send_to == "cluster": query = ( select(distinct(User.tg_id)) .join(Key, User.tg_id == Key.tg_id) .join(Server, Key.server_id == Server.cluster_name) .where(Server.cluster_name == cluster_name) + .where(~User.tg_id.in_(banned_tg_ids)) ) elif send_to == "hotleads": subquery = select(Key.tg_id) @@ -268,6 +277,7 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, .join(Payment, User.tg_id == Payment.tg_id) .where(Payment.status == "success") .where(~User.tg_id.in_(subquery)) + .where(~User.tg_id.in_(banned_tg_ids)) ) elif send_to == "trial": trial_tariff_subquery = select(Tariff.id).where(Tariff.group_code == "trial") @@ -275,9 +285,10 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, query = ( select(distinct(Key.tg_id)) .where(Key.tariff_id.in_(trial_tariff_subquery)) + .where(~Key.tg_id.in_(banned_tg_ids)) ) else: - query = select(distinct(User.tg_id)) + query = select(distinct(User.tg_id)).where(~User.tg_id.in_(banned_tg_ids)) result = await session.execute(query) tg_ids = [row[0] for row in result.all()] diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py index fd19f971..713b0d34 100644 --- a/handlers/admin/stats/stats_handler.py +++ b/handlers/admin/stats/stats_handler.py @@ -203,7 +203,19 @@ async def handle_stats(callback_query: CallbackQuery, session: AsyncSession): if extra_blocks: stats_message += "\n\n" + "\n\n".join([str(b) for b in extra_blocks if b]) - await callback_query.message.edit_text(text=stats_message, reply_markup=build_stats_kb()) + new_kb = build_stats_kb() + current_text = callback_query.message.html_text or callback_query.message.text or "" + cur_kb = callback_query.message.reply_markup + cur_kb_json = cur_kb.model_dump_json() if cur_kb else None + new_kb_json = new_kb.model_dump_json() if new_kb else None + + if current_text == stats_message and cur_kb_json == new_kb_json: + try: + await callback_query.answer() + except Exception: + pass + else: + await callback_query.message.edit_text(text=stats_message, reply_markup=new_kb) except TelegramBadRequest as e: if "message is not modified" not in str(e): diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index 0be75f32..8fed7bcf 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -245,12 +245,6 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data.startswith("select_country|")) async def handle_country_selection(callback_query: CallbackQuery, session: Any, state: FSMContext): - """ - Обрабатывает выбор страны. - Формат callback data: - select_country|{selected_country}|{ts} [|{old_key_name} (опционально)] - Если передан old_key_name – значит, происходит смена локации. - """ data = callback_query.data.split("|") if len(data) < 3: await callback_query.message.answer("❌ Некорректные данные. Попробуйте снова.") @@ -263,23 +257,41 @@ async def handle_country_selection(callback_query: CallbackQuery, session: Any, await callback_query.message.answer("❌ Некорректное время истечения. Попробуйте снова.") return - expiry_time = datetime.fromtimestamp(ts, tz=moscow_tz) - old_key_name = data[3] if len(data) > 3 else None - tg_id = callback_query.from_user.id - logger.info(f"Пользователь {tg_id} выбрал страну: {selected_country}") - logger.info(f"Получено время истечения (timestamp): {ts}") - await finalize_key_creation( - tg_id, - expiry_time, - selected_country, - state, - session, - callback_query, - old_key_name, - ) + fsm_data = await state.get_data() + if fsm_data.get("creating_key"): + try: + await callback_query.answer("⏳ Уже обрабатываю…") + except Exception: + pass + return + + await state.update_data(creating_key=True) + + try: + await callback_query.answer("Обрабатываю…") + if callback_query.message: + await callback_query.message.edit_reply_markup(reply_markup=None) + except Exception: + pass + + try: + expiry_time = datetime.fromtimestamp(ts, tz=moscow_tz) + await finalize_key_creation( + tg_id, + expiry_time, + selected_country, + state, + session, + callback_query, + old_key_name, + ) + finally: + fsm_data = await state.get_data() + if fsm_data.get("creating_key"): + await state.update_data(creating_key=False) async def finalize_key_creation( diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index b32933bc..fd3ac0d6 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -338,7 +338,11 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: tariff = await get_tariff_by_id(session, tariff_id) if not tariff: - await callback_query.message.edit_text("❌ Указанный тариф не найден.") + await edit_or_send_message( + target_message=callback_query.message, + text="❌ Указанный тариф не найден.", + ) + await callback_query.answer() return discount_info = await check_hot_lead_discount(session, tg_id) @@ -346,18 +350,18 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: if not discount_info.get("available") or datetime.utcnow() >= discount_info["expires_at"]: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - - await callback_query.message.edit_text( - "❌ Скидка недоступна или истекла. Пожалуйста, выберите тариф заново.", - reply_markup=builder.as_markup() + await edit_or_send_message( + target_message=callback_query.message, + text="❌ Скидка недоступна или истекла. Пожалуйста, выберите тариф заново.", + reply_markup=builder.as_markup(), ) + await callback_query.answer() return duration_days = tariff["duration_days"] price_rub = tariff["price_rub"] balance = await get_balance(session, tg_id) - price_rub = tariff["price_rub"] if balance < price_rub: required_amount = ceil(price_rub - balance) @@ -374,7 +378,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: module_fast_flow_handlers = load_module_fast_flow_handlers() flow_handled = False - + if USE_NEW_PAYMENT_FLOW in module_fast_flow_handlers: try: handler = module_fast_flow_handlers[USE_NEW_PAYMENT_FLOW] @@ -413,6 +417,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: text=CREATING_CONNECTION_MSG, reply_markup=builder.as_markup(), ) + await callback_query.answer() expiry_time = datetime.now(moscow_tz) + timedelta(days=duration_days) await state.update_data(tariff_id=tariff_id) diff --git a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so index 3e741802..d73dd07e 100644 Binary files a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so differ diff --git a/handlers/payments/kassai.py b/handlers/payments/kassai.py index fdfc6b5e..528e2fe3 100644 --- a/handlers/payments/kassai.py +++ b/handlers/payments/kassai.py @@ -49,7 +49,10 @@ async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state if method_name: method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None) if not method: - await callback_query.message.delete() + try: + await callback_query.message.delete() + except Exception: + pass await callback_query.message.answer( target_message=callback_query.message, text="Ошибка: выбранный способ оплаты недоступен.", @@ -80,7 +83,10 @@ async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"kassai_custom_amount|{method_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) - await callback_query.message.delete() + try: + await callback_query.message.delete() + except Exception: + pass new_msg = await callback_query.message.answer( target_message=callback_query.message, text=method["desc"], @@ -101,7 +107,10 @@ async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state builder.row(InlineKeyboardButton(text=method["button"], callback_data=f'kassai_method|{method["name"]}')) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) - await callback_query.message.delete() + try: + await callback_query.message.delete() + except Exception: + pass new_msg = await callback_query.message.answer( target_message=callback_query.message, text="Выберите способ оплаты через KassaAI:", diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 6be02a0b..57d8c3d4 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -255,7 +255,7 @@ async def robokassa_webhook(request: web.Request): logger.info(f"Processing payment for user {tg_id} with amount {amount}.") async with async_session_maker() as session: - recent_time = datetime.now(MOSCOW_TZ).replace(tzinfo=None) - timedelta(minutes=1) + recent_time = datetime.now(MOSCOW_TZ).replace(tzinfo=None) - timedelta(seconds=10) result = await session.execute( select(Payment).where( diff --git a/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so b/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so index 2d39744b..5556d37d 100644 Binary files a/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so differ diff --git a/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so b/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so index 6dd32ef1..05a6335b 100644 Binary files a/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so differ diff --git a/middlewares/direct_start_blocker.py b/middlewares/direct_start_blocker.py index 8f539746..f4f9d3c6 100644 --- a/middlewares/direct_start_blocker.py +++ b/middlewares/direct_start_blocker.py @@ -4,6 +4,7 @@ from typing import Any from aiogram import BaseMiddleware from aiogram.types import Message, Update from sqlalchemy import select +from datetime import datetime from config import DISABLE_DIRECT_START from database import async_session_maker, check_user_exists @@ -60,9 +61,15 @@ class DirectStartBlockerMiddleware(BaseMiddleware): elif start_param.startswith("gift_"): gift_id = start_param.removeprefix("gift_") - result = await session.execute(select(Gift).where(Gift.id == gift_id)) + result = await session.execute( + select(Gift).where( + Gift.gift_id == gift_id, + Gift.is_used.is_(False), + (Gift.expiry_time.is_(None)) | (Gift.expiry_time > datetime.utcnow()), + ) + ) if not result.scalar_one_or_none(): - logger.info(f"[DirectStartBlocker] Подарок не найден: {gift_id!r}") + logger.info(f"[DirectStartBlocker] Подарок неактивен или не найден: {gift_id!r}") return elif start_param.startswith("referral_"): @@ -92,4 +99,4 @@ class DirectStartBlockerMiddleware(BaseMiddleware): ) return - return await handler(event, data) + return