diff --git a/database.py b/database.py index b9ffb737..9eca980a 100644 --- a/database.py +++ b/database.py @@ -1619,3 +1619,16 @@ async def get_tariffs_for_cluster(session, cluster_name: str) -> list[dict]: async def get_tariff_by_id(session, tariff_id: int) -> dict | None: row = await session.fetchrow("SELECT * FROM tariffs WHERE id = $1", tariff_id) return dict(row) if row else None + + +async def get_hot_leads(conn): + """ + Возвращает пользователей, у которых есть успешные оплаты, но нет активных ключей. + """ + query = """ + SELECT DISTINCT p.tg_id + FROM payments p + LEFT JOIN keys k ON p.tg_id = k.tg_id AND k.expiry_time > EXTRACT(EPOCH FROM now()) * 1000 + WHERE p.amount > 0 AND k.tg_id IS NULL + """ + return await conn.fetch(query) \ No newline at end of file diff --git a/handlers/buttons.py b/handlers/buttons.py index 5e97de69..31d10839 100644 --- a/handlers/buttons.py +++ b/handlers/buttons.py @@ -30,7 +30,8 @@ YOOMONEY = "💳 ЮМани: перевод по карте" CRYPTOBOT = "💰 CryptoBot: криптовалюта" STARS = "⭐ Оплата Звездами" ROBOKASSA = "⭐ RoboKassa" - +DISCOUNT_TARIFF = "🔥 Получить скидку" +MAX_DISCOUNT_TARIFF = "⚡ Получить максимальную скидку" # Подарки diff --git a/handlers/keys/key_mode/__init__.py b/handlers/keys/key_mode/__init__.py index 763fcfd7..1287d9bd 100644 --- a/handlers/keys/key_mode/__init__.py +++ b/handlers/keys/key_mode/__init__.py @@ -5,6 +5,7 @@ from aiogram import Router from .key_cluster_mode import router as cluster_router from .key_country_mode import router as country_router from .key_create import router as create_router +from .key_discount_mode import router as discount_router router = Router(name="key_mode_router") @@ -13,4 +14,5 @@ router.include_routers( create_router, cluster_router, country_router, + discount_router ) diff --git a/handlers/keys/key_mode/key_discount_mode.py b/handlers/keys/key_mode/key_discount_mode.py new file mode 100644 index 00000000..b483b79c --- /dev/null +++ b/handlers/keys/key_mode/key_discount_mode.py @@ -0,0 +1,91 @@ +from aiogram import F, Router +from aiogram.types import CallbackQuery + +from database import get_tariffs +from handlers.notifications.notify_kb import build_tariffs_keyboard +from .key_create import select_tariff_plan +from logger import logger +from datetime import datetime, timedelta +from config import DISCOUNT_ACTIVE_HOURS +from handlers.texts import DISCOUNT_TARIFF, DISCOUNT_TARIFF_MAX + +router = Router() + + +@router.callback_query(F.data == "hot_lead_discount") +async def handle_discount_entry(callback: CallbackQuery, session): + tg_id = callback.from_user.id + + last_time = await session.fetchval(""" + SELECT last_notification_time + FROM notifications + WHERE tg_id = $1 AND notification_type = 'hot_lead_step_2' + """, tg_id) + + if not last_time: + await callback.message.edit_text("❌ Скидка недоступна.") + return + + now = datetime.utcnow() + if now - last_time > timedelta(hours=DISCOUNT_ACTIVE_HOURS): + await callback.message.edit_text("⏳ Срок действия скидки истёк.") + return + + tariffs = await get_tariffs(session=session, group_code="discounts") + + if not tariffs: + await callback.message.edit_text("❌ Скидочные тарифы временно недоступны.") + return + + await callback.message.edit_text( + DISCOUNT_TARIFF, + reply_markup=build_tariffs_keyboard(tariffs, prefix="discount_tariff") + ) + + +@router.callback_query(F.data.startswith("discount_tariff|")) +async def handle_discount_tariff_selection(callback: CallbackQuery, session, state): + try: + tariff_id = int(callback.data.split("|")[1]) + fake_callback = CallbackQuery.model_construct( + id=callback.id, + from_user=callback.from_user, + chat_instance=callback.chat_instance, + message=callback.message, + data=f"select_tariff_plan|{tariff_id}", + ) + await select_tariff_plan(fake_callback, session=session, state=state) + + except Exception as e: + logger.error(f"Ошибка при выборе скидочного тарифа: {e}") + await callback.message.answer("❌ Произошла ошибка при выборе тарифа.") + + +@router.callback_query(F.data == "hot_lead_final_discount") +async def handle_ultra_discount(callback: CallbackQuery, session): + tg_id = callback.from_user.id + + last_time = await session.fetchval(""" + SELECT last_notification_time + FROM notifications + WHERE tg_id = $1 AND notification_type = 'hot_lead_step_3' + """, tg_id) + + if not last_time: + await callback.message.edit_text("❌ Скидка недоступна.") + return + + now = datetime.utcnow() + if now - last_time > timedelta(hours=DISCOUNT_ACTIVE_HOURS): + await callback.message.edit_text("⏳ Срок действия финальной скидки истёк.") + return + + tariffs = await get_tariffs(session, group_code="discounts_max") + if not tariffs: + await callback.message.edit_text("❌ Скидочные тарифы временно недоступны.") + return + + await callback.message.edit_text( + DISCOUNT_TARIFF_MAX, + reply_markup=build_tariffs_keyboard(tariffs, prefix="discount_tariff") + ) diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index 584e593d..850d1380 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -17,6 +17,7 @@ from config import ( NOTIFY_RENEW, NOTIFY_RENEW_EXPIRED, TRIAL_TIME_DISABLE, + NOTIFY_HOT_LEADS ) from database import ( add_notification, @@ -52,6 +53,7 @@ from logger import logger from .notify_utils import send_messages_with_limit, send_notification from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic +from .hot_leads_notifications import notify_hot_leads router = Router() @@ -118,6 +120,12 @@ async def periodic_notifications(bot: Bot): except Exception as e: logger.error(f"Ошибка в notify_users_no_traffic: {e}") await asyncio.sleep(0.5) + if NOTIFY_HOT_LEADS: + try: + await notify_hot_leads(bot) + except Exception as e: + logger.error(f"Ошибка в notify_hot_leads: {e}") + await asyncio.sleep(0.5) logger.info("Завершена обработка уведомлений") diff --git a/handlers/notifications/hot_leads_notifications.py b/handlers/notifications/hot_leads_notifications.py new file mode 100644 index 00000000..7baaf1b3 --- /dev/null +++ b/handlers/notifications/hot_leads_notifications.py @@ -0,0 +1,81 @@ +import asyncpg +from aiogram import Bot + +from config import DATABASE_URL, HOT_LEAD_INTERVAL_HOURS +from database import check_notification_time, add_notification, get_hot_leads +from handlers.notifications.notify_utils import send_notification +from logger import logger +from handlers.notifications.notify_kb import build_hot_lead_kb +from handlers.texts import HOT_LEAD_MESSAGE, HOT_LEAD_FINAL_MESSAGE + + +async def notify_hot_leads(bot: Bot): + logger.info("🚀 Запуск уведомлений для горячих лидов.") + conn = await asyncpg.connect(DATABASE_URL) + + try: + leads = await get_hot_leads(conn) + notified = 0 + + for row in leads: + tg_id = row["tg_id"] + + has_step_1 = await conn.fetchval( + "SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = $1 AND notification_type = 'hot_lead_step_1')", + tg_id + ) + if not has_step_1: + await add_notification(tg_id, "hot_lead_step_1", session=conn) + logger.info(f"[HOT LEAD] Шаг 1 — зафиксировано без отправки: {tg_id}") + continue + + has_step_2 = await conn.fetchval( + "SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = $1 AND notification_type = 'hot_lead_step_2')", + tg_id + ) + if not has_step_2: + can_send = await check_notification_time( + tg_id=tg_id, + notification_type="hot_lead_step_1", + hours=HOT_LEAD_INTERVAL_HOURS, + session=conn + ) + if not can_send: + continue + + keyboard = build_hot_lead_kb() + result = await send_notification(bot, tg_id, None, HOT_LEAD_MESSAGE, keyboard) + if result: + await add_notification(tg_id, "hot_lead_step_2", session=conn) + logger.info(f"🔥 Шаг 2 — отправлено первое уведомление: {tg_id}") + notified += 1 + continue + + has_step_3 = await conn.fetchval( + "SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = $1 AND notification_type = 'hot_lead_step_3')", + tg_id + ) + if not has_step_3: + can_send = await check_notification_time( + tg_id=tg_id, + notification_type="hot_lead_step_2", + hours=HOT_LEAD_INTERVAL_HOURS, + session=conn + ) + if not can_send: + continue + + keyboard = build_hot_lead_kb(final=True) + result = await send_notification(bot, tg_id, None, HOT_LEAD_FINAL_MESSAGE, keyboard) + if result: + await add_notification(tg_id, "hot_lead_step_3", session=conn) + logger.info(f"⚡ Шаг 3 — отправлено финальное уведомление: {tg_id}") + notified += 1 + + logger.info(f"✅ Уведомления завершены. Отправлено: {notified}") + + except Exception as e: + logger.error(f"❌ Ошибка в notify_hot_leads: {e}") + finally: + await conn.close() + diff --git a/handlers/notifications/notify_kb.py b/handlers/notifications/notify_kb.py index 6fa7bad7..2c7ba04e 100644 --- a/handlers/notifications/notify_kb.py +++ b/handlers/notifications/notify_kb.py @@ -1,6 +1,7 @@ -from aiogram.types import InlineKeyboardMarkup +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from handlers.buttons import MAIN_MENU, RENEW_KEY +from handlers.buttons import DISCOUNT_TARIFF, MAX_DISCOUNT_TARIFF def build_notification_kb(email: str) -> InlineKeyboardMarkup: @@ -27,3 +28,24 @@ def build_notification_expired_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text=MAIN_MENU, callback_data="profile") return builder.as_markup() + + +def build_hot_lead_kb(final: bool = False) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton( + text=DISCOUNT_TARIFF if not final else MAX_DISCOUNT_TARIFF, + callback_data="hot_lead_discount" if not final else "hot_lead_final_discount" + )] + ]) + + +def build_tariffs_keyboard(tariffs: list[dict], prefix: str = "tariff") -> InlineKeyboardMarkup: + buttons = [ + [InlineKeyboardButton( + text=f"{t['name']} — {t['price_rub']}₽", + callback_data=f"{prefix}|{t['id']}" + )] + for t in tariffs + ] + + return InlineKeyboardMarkup(inline_keyboard=buttons) diff --git a/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so b/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so index 360c1436..16e9de4a 100644 Binary files a/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so differ