discounts tariffs/notify hot leads/change success payment
This commit is contained in:
+13
@@ -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)
|
||||
+2
-1
@@ -30,7 +30,8 @@ YOOMONEY = "💳 ЮМани: перевод по карте"
|
||||
CRYPTOBOT = "💰 CryptoBot: криптовалюта"
|
||||
STARS = "⭐ Оплата Звездами"
|
||||
ROBOKASSA = "⭐ RoboKassa"
|
||||
|
||||
DISCOUNT_TARIFF = "🔥 Получить скидку"
|
||||
MAX_DISCOUNT_TARIFF = "⚡ Получить максимальную скидку"
|
||||
|
||||
# Подарки
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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("Завершена обработка уведомлений")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user