6392033579
- Заменён метод send_notification на send_to_admins в AdminNotificationService - Исправлена настройка NOTIFICATIONS_CHAT_ID на ADMIN_NOTIFICATIONS_CHAT_ID для отправки в топик
922 lines
33 KiB
Python
922 lines
33 KiB
Python
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import qrcode
|
|
from aiogram import Dispatcher, F, types
|
|
from aiogram.exceptions import TelegramBadRequest
|
|
from aiogram.fsm.context import FSMContext
|
|
from aiogram.types import FSInputFile
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.database.models import User
|
|
from app.keyboards.inline import get_referral_keyboard
|
|
from app.localization.texts import get_texts
|
|
from app.services.referral_withdrawal_service import referral_withdrawal_service
|
|
from app.services.admin_notification_service import AdminNotificationService
|
|
from app.states import ReferralWithdrawalStates
|
|
from app.utils.photo_message import edit_or_answer_photo
|
|
from app.utils.user_utils import (
|
|
get_detailed_referral_list,
|
|
get_effective_referral_commission_percent,
|
|
get_referral_analytics,
|
|
get_user_referral_summary,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def show_referral_info(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
db: AsyncSession
|
|
):
|
|
# Проверяем, включена ли реферальная программа
|
|
if not settings.is_referral_program_enabled():
|
|
texts = get_texts(db_user.language)
|
|
await callback.answer(
|
|
texts.t("REFERRAL_PROGRAM_DISABLED", "Реферальная программа отключена"),
|
|
show_alert=True
|
|
)
|
|
return
|
|
|
|
texts = get_texts(db_user.language)
|
|
|
|
summary = await get_user_referral_summary(db, db_user.id)
|
|
|
|
bot_username = (await callback.bot.get_me()).username
|
|
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
|
|
|
|
referral_text = (
|
|
texts.t("REFERRAL_PROGRAM_TITLE", "👥 <b>Реферальная программа</b>")
|
|
+ "\n\n"
|
|
+ texts.t("REFERRAL_STATS_HEADER", "📊 <b>Ваша статистика:</b>")
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_STATS_INVITED",
|
|
"• Приглашено пользователей: <b>{count}</b>",
|
|
).format(count=summary['invited_count'])
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_STATS_FIRST_TOPUPS",
|
|
"• Сделали первое пополнение: <b>{count}</b>",
|
|
).format(count=summary['paid_referrals_count'])
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_STATS_ACTIVE",
|
|
"• Активных рефералов: <b>{count}</b>",
|
|
).format(count=summary['active_referrals_count'])
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_STATS_CONVERSION",
|
|
"• Конверсия: <b>{rate}%</b>",
|
|
).format(rate=summary['conversion_rate'])
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_STATS_TOTAL_EARNED",
|
|
"• Заработано всего: <b>{amount}</b>",
|
|
).format(amount=texts.format_price(summary['total_earned_kopeks']))
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_STATS_MONTH_EARNED",
|
|
"• За последний месяц: <b>{amount}</b>",
|
|
).format(amount=texts.format_price(summary['month_earned_kopeks']))
|
|
+ "\n\n"
|
|
+ texts.t("REFERRAL_REWARDS_HEADER", "🎁 <b>Как работают награды:</b>")
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_REWARD_NEW_USER",
|
|
"• Новый пользователь получает: <b>{bonus}</b> при первом пополнении от <b>{minimum}</b>",
|
|
).format(
|
|
bonus=texts.format_price(settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS),
|
|
minimum=texts.format_price(settings.REFERRAL_MINIMUM_TOPUP_KOPEKS),
|
|
)
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_REWARD_INVITER",
|
|
"• Вы получаете при первом пополнении реферала: <b>{bonus}</b>",
|
|
).format(bonus=texts.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS))
|
|
+ "\n"
|
|
+ texts.t(
|
|
"REFERRAL_REWARD_COMMISSION",
|
|
"• Комиссия с каждого пополнения реферала: <b>{percent}%</b>",
|
|
).format(percent=get_effective_referral_commission_percent(db_user))
|
|
+ "\n\n"
|
|
+ texts.t("REFERRAL_LINK_TITLE", "🔗 <b>Ваша реферальная ссылка:</b>")
|
|
+ f"\n<code>{referral_link}</code>\n\n"
|
|
+ texts.t("REFERRAL_CODE_TITLE", "🆔 <b>Ваш код:</b> <code>{code}</code>").format(code=db_user.referral_code)
|
|
+ "\n\n"
|
|
)
|
|
|
|
if summary['recent_earnings']:
|
|
meaningful_earnings = [
|
|
earning for earning in summary['recent_earnings'][:5]
|
|
if earning['amount_kopeks'] > 0
|
|
]
|
|
|
|
if meaningful_earnings:
|
|
referral_text += texts.t(
|
|
"REFERRAL_RECENT_EARNINGS_HEADER",
|
|
"💰 <b>Последние начисления:</b>",
|
|
) + "\n"
|
|
for earning in meaningful_earnings[:3]:
|
|
reason_text = {
|
|
"referral_first_topup": texts.t(
|
|
"REFERRAL_EARNING_REASON_FIRST_TOPUP",
|
|
"🎉 Первое пополнение",
|
|
),
|
|
"referral_commission_topup": texts.t(
|
|
"REFERRAL_EARNING_REASON_COMMISSION_TOPUP",
|
|
"💰 Комиссия с пополнения",
|
|
),
|
|
"referral_commission": texts.t(
|
|
"REFERRAL_EARNING_REASON_COMMISSION_PURCHASE",
|
|
"💰 Комиссия с покупки",
|
|
),
|
|
}.get(earning['reason'], earning['reason'])
|
|
|
|
referral_text += texts.t(
|
|
"REFERRAL_RECENT_EARNINGS_ITEM",
|
|
"• {reason}: <b>{amount}</b> от {referral_name}",
|
|
).format(
|
|
reason=reason_text,
|
|
amount=texts.format_price(earning['amount_kopeks']),
|
|
referral_name=earning['referral_name'],
|
|
) + "\n"
|
|
referral_text += "\n"
|
|
|
|
if summary['earnings_by_type']:
|
|
referral_text += texts.t(
|
|
"REFERRAL_EARNINGS_BY_TYPE_HEADER",
|
|
"📈 <b>Доходы по типам:</b>",
|
|
) + "\n"
|
|
|
|
if 'referral_first_topup' in summary['earnings_by_type']:
|
|
data = summary['earnings_by_type']['referral_first_topup']
|
|
if data['total_amount_kopeks'] > 0:
|
|
referral_text += texts.t(
|
|
"REFERRAL_EARNINGS_FIRST_TOPUPS",
|
|
"• Бонусы за первые пополнения: <b>{count}</b> ({amount})",
|
|
).format(
|
|
count=data['count'],
|
|
amount=texts.format_price(data['total_amount_kopeks']),
|
|
) + "\n"
|
|
|
|
if 'referral_commission_topup' in summary['earnings_by_type']:
|
|
data = summary['earnings_by_type']['referral_commission_topup']
|
|
if data['total_amount_kopeks'] > 0:
|
|
referral_text += texts.t(
|
|
"REFERRAL_EARNINGS_TOPUPS",
|
|
"• Комиссии с пополнений: <b>{count}</b> ({amount})",
|
|
).format(
|
|
count=data['count'],
|
|
amount=texts.format_price(data['total_amount_kopeks']),
|
|
) + "\n"
|
|
|
|
if 'referral_commission' in summary['earnings_by_type']:
|
|
data = summary['earnings_by_type']['referral_commission']
|
|
if data['total_amount_kopeks'] > 0:
|
|
referral_text += texts.t(
|
|
"REFERRAL_EARNINGS_PURCHASES",
|
|
"• Комиссии с покупок: <b>{count}</b> ({amount})",
|
|
).format(
|
|
count=data['count'],
|
|
amount=texts.format_price(data['total_amount_kopeks']),
|
|
) + "\n"
|
|
|
|
referral_text += "\n"
|
|
|
|
referral_text += texts.t(
|
|
"REFERRAL_INVITE_FOOTER",
|
|
"📢 Приглашайте друзей и зарабатывайте!",
|
|
)
|
|
|
|
await edit_or_answer_photo(
|
|
callback,
|
|
referral_text,
|
|
get_referral_keyboard(db_user.language),
|
|
)
|
|
await callback.answer()
|
|
|
|
|
|
async def show_referral_qr(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
):
|
|
await callback.answer()
|
|
|
|
texts = get_texts(db_user.language)
|
|
|
|
bot_username = (await callback.bot.get_me()).username
|
|
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
|
|
|
|
qr_dir = Path("data") / "referral_qr"
|
|
qr_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
file_path = qr_dir / f"{db_user.id}.png"
|
|
if not file_path.exists():
|
|
img = qrcode.make(referral_link)
|
|
img.save(file_path)
|
|
|
|
photo = FSInputFile(file_path)
|
|
keyboard = types.InlineKeyboardMarkup(
|
|
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_referrals")]]
|
|
)
|
|
|
|
try:
|
|
await callback.message.edit_media(
|
|
types.InputMediaPhoto(
|
|
media=photo,
|
|
caption=texts.t(
|
|
"REFERRAL_LINK_CAPTION",
|
|
"🔗 Ваша реферальная ссылка:\n{link}",
|
|
).format(link=referral_link),
|
|
),
|
|
reply_markup=keyboard,
|
|
)
|
|
except TelegramBadRequest:
|
|
await callback.message.delete()
|
|
await callback.message.answer_photo(
|
|
photo,
|
|
caption=texts.t(
|
|
"REFERRAL_LINK_CAPTION",
|
|
"🔗 Ваша реферальная ссылка:\n{link}",
|
|
).format(link=referral_link),
|
|
reply_markup=keyboard,
|
|
)
|
|
|
|
|
|
async def show_detailed_referral_list(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
db: AsyncSession,
|
|
page: int = 1
|
|
):
|
|
texts = get_texts(db_user.language)
|
|
|
|
referrals_data = await get_detailed_referral_list(db, db_user.id, limit=10, offset=(page - 1) * 10)
|
|
|
|
if not referrals_data['referrals']:
|
|
await edit_or_answer_photo(
|
|
callback,
|
|
texts.t(
|
|
"REFERRAL_LIST_EMPTY",
|
|
"📋 У вас пока нет рефералов.\n\nПоделитесь своей реферальной ссылкой, чтобы начать зарабатывать!",
|
|
),
|
|
types.InlineKeyboardMarkup(
|
|
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_referrals")]]
|
|
),
|
|
parse_mode=None,
|
|
)
|
|
await callback.answer()
|
|
return
|
|
|
|
text = texts.t(
|
|
"REFERRAL_LIST_HEADER",
|
|
"👥 <b>Ваши рефералы</b> (стр. {current}/{total})",
|
|
).format(
|
|
current=referrals_data['current_page'],
|
|
total=referrals_data['total_pages'],
|
|
) + "\n\n"
|
|
|
|
for i, referral in enumerate(referrals_data['referrals'], 1):
|
|
status_emoji = "🟢" if referral['status'] == 'active' else "🔴"
|
|
|
|
topup_emoji = "💰" if referral['has_made_first_topup'] else "⏳"
|
|
|
|
text += texts.t(
|
|
"REFERRAL_LIST_ITEM_HEADER",
|
|
"{index}. {status} <b>{name}</b>",
|
|
).format(index=i, status=status_emoji, name=referral['full_name']) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_LIST_ITEM_TOPUPS",
|
|
" {emoji} Пополнений: {count}",
|
|
).format(emoji=topup_emoji, count=referral['topups_count']) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_LIST_ITEM_EARNED",
|
|
" 💎 Заработано с него: {amount}",
|
|
).format(amount=texts.format_price(referral['total_earned_kopeks'])) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_LIST_ITEM_REGISTERED",
|
|
" 📅 Регистрация: {days} дн. назад",
|
|
).format(days=referral['days_since_registration']) + "\n"
|
|
|
|
if referral['days_since_activity'] is not None:
|
|
text += texts.t(
|
|
"REFERRAL_LIST_ITEM_ACTIVITY",
|
|
" 🕐 Активность: {days} дн. назад",
|
|
).format(days=referral['days_since_activity']) + "\n"
|
|
else:
|
|
text += texts.t(
|
|
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO",
|
|
" 🕐 Активность: давно",
|
|
) + "\n"
|
|
|
|
text += "\n"
|
|
|
|
keyboard = []
|
|
nav_buttons = []
|
|
|
|
if referrals_data['has_prev']:
|
|
nav_buttons.append(types.InlineKeyboardButton(
|
|
text=texts.t("REFERRAL_LIST_PREV_PAGE", "⬅️ Назад"),
|
|
callback_data=f"referral_list_page_{page - 1}"
|
|
))
|
|
|
|
if referrals_data['has_next']:
|
|
nav_buttons.append(types.InlineKeyboardButton(
|
|
text=texts.t("REFERRAL_LIST_NEXT_PAGE", "Вперед ➡️"),
|
|
callback_data=f"referral_list_page_{page + 1}"
|
|
))
|
|
|
|
if nav_buttons:
|
|
keyboard.append(nav_buttons)
|
|
|
|
keyboard.append([types.InlineKeyboardButton(
|
|
text=texts.BACK,
|
|
callback_data="menu_referrals"
|
|
)])
|
|
|
|
await edit_or_answer_photo(
|
|
callback,
|
|
text,
|
|
types.InlineKeyboardMarkup(inline_keyboard=keyboard),
|
|
)
|
|
await callback.answer()
|
|
|
|
|
|
async def show_referral_analytics(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
db: AsyncSession
|
|
):
|
|
texts = get_texts(db_user.language)
|
|
|
|
analytics = await get_referral_analytics(db, db_user.id)
|
|
|
|
text = texts.t("REFERRAL_ANALYTICS_TITLE", "📊 <b>Аналитика рефералов</b>") + "\n\n"
|
|
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_EARNINGS_HEADER",
|
|
"💰 <b>Доходы по периодам:</b>",
|
|
) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_EARNINGS_TODAY",
|
|
"• Сегодня: {amount}",
|
|
).format(amount=texts.format_price(analytics['earnings_by_period']['today'])) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_EARNINGS_WEEK",
|
|
"• За неделю: {amount}",
|
|
).format(amount=texts.format_price(analytics['earnings_by_period']['week'])) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_EARNINGS_MONTH",
|
|
"• За месяц: {amount}",
|
|
).format(amount=texts.format_price(analytics['earnings_by_period']['month'])) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_EARNINGS_QUARTER",
|
|
"• За квартал: {amount}",
|
|
).format(amount=texts.format_price(analytics['earnings_by_period']['quarter'])) + "\n\n"
|
|
|
|
if analytics['top_referrals']:
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_TOP_TITLE",
|
|
"🏆 <b>Топ-{count} рефералов:</b>",
|
|
).format(count=len(analytics['top_referrals'])) + "\n"
|
|
for i, ref in enumerate(analytics['top_referrals'], 1):
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_TOP_ITEM",
|
|
"{index}. {name}: {amount} ({count} начислений)",
|
|
).format(
|
|
index=i,
|
|
name=ref['referral_name'],
|
|
amount=texts.format_price(ref['total_earned_kopeks']),
|
|
count=ref['earnings_count'],
|
|
) + "\n"
|
|
text += "\n"
|
|
|
|
text += texts.t(
|
|
"REFERRAL_ANALYTICS_FOOTER",
|
|
"📈 Продолжайте развивать свою реферальную сеть!",
|
|
)
|
|
|
|
await edit_or_answer_photo(
|
|
callback,
|
|
text,
|
|
types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_referrals")]
|
|
]),
|
|
)
|
|
await callback.answer()
|
|
|
|
|
|
async def create_invite_message(
|
|
callback: types.CallbackQuery,
|
|
db_user: User
|
|
):
|
|
texts = get_texts(db_user.language)
|
|
|
|
bot_username = (await callback.bot.get_me()).username
|
|
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
|
|
|
|
invite_text = (
|
|
texts.t("REFERRAL_INVITE_TITLE", "🎉 Присоединяйся к VPN сервису!")
|
|
+ "\n\n"
|
|
+ texts.t(
|
|
"REFERRAL_INVITE_BONUS",
|
|
"💎 При первом пополнении от {minimum} ты получишь {bonus} бонусом на баланс!",
|
|
).format(
|
|
minimum=texts.format_price(settings.REFERRAL_MINIMUM_TOPUP_KOPEKS),
|
|
bonus=texts.format_price(settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS),
|
|
)
|
|
+ "\n\n"
|
|
+ texts.t("REFERRAL_INVITE_FEATURE_FAST", "🚀 Быстрое подключение")
|
|
+ "\n"
|
|
+ texts.t("REFERRAL_INVITE_FEATURE_SERVERS", "🌍 Серверы по всему миру")
|
|
+ "\n"
|
|
+ texts.t("REFERRAL_INVITE_FEATURE_SECURE", "🔒 Надежная защита")
|
|
+ "\n\n"
|
|
+ texts.t("REFERRAL_INVITE_LINK_PROMPT", "👇 Переходи по ссылке:")
|
|
+ f"\n{referral_link}"
|
|
)
|
|
|
|
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(
|
|
text=texts.t("REFERRAL_SHARE_BUTTON", "📤 Поделиться"),
|
|
switch_inline_query=invite_text
|
|
)],
|
|
[types.InlineKeyboardButton(
|
|
text=texts.BACK,
|
|
callback_data="menu_referrals"
|
|
)]
|
|
])
|
|
|
|
await edit_or_answer_photo(
|
|
callback,
|
|
(
|
|
texts.t("REFERRAL_INVITE_CREATED_TITLE", "📝 <b>Приглашение создано!</b>")
|
|
+ "\n\n"
|
|
+ texts.t(
|
|
"REFERRAL_INVITE_CREATED_INSTRUCTION",
|
|
"Нажмите кнопку «📤 Поделиться» чтобы отправить приглашение в любой чат, или скопируйте текст ниже:",
|
|
)
|
|
+ "\n\n"
|
|
f"<code>{invite_text}</code>"
|
|
),
|
|
keyboard,
|
|
)
|
|
await callback.answer()
|
|
|
|
|
|
async def show_withdrawal_info(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
db: AsyncSession,
|
|
state: FSMContext
|
|
):
|
|
"""Показывает информацию о выводе реферального баланса."""
|
|
texts = get_texts(db_user.language)
|
|
|
|
if not settings.is_referral_withdrawal_enabled():
|
|
await callback.answer(
|
|
texts.t("REFERRAL_WITHDRAWAL_DISABLED", "Функция вывода отключена"),
|
|
show_alert=True
|
|
)
|
|
return
|
|
|
|
# Получаем детальную статистику баланса
|
|
stats = await referral_withdrawal_service.get_referral_balance_stats(db, db_user.id)
|
|
min_amount = settings.REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS
|
|
cooldown_days = settings.REFERRAL_WITHDRAWAL_COOLDOWN_DAYS
|
|
|
|
# Проверяем возможность вывода
|
|
can_request, reason = await referral_withdrawal_service.can_request_withdrawal(db, db_user.id)
|
|
|
|
text = texts.t("REFERRAL_WITHDRAWAL_TITLE", "💸 <b>Вывод реферального баланса</b>") + "\n\n"
|
|
|
|
# Показываем детальную статистику
|
|
text += referral_withdrawal_service.format_balance_stats_for_user(stats, texts)
|
|
text += "\n"
|
|
|
|
text += texts.t(
|
|
"REFERRAL_WITHDRAWAL_MIN_AMOUNT",
|
|
"📊 Минимальная сумма: <b>{amount}</b>"
|
|
).format(amount=texts.format_price(min_amount)) + "\n"
|
|
text += texts.t(
|
|
"REFERRAL_WITHDRAWAL_COOLDOWN",
|
|
"⏱ Частота вывода: раз в <b>{days}</b> дней"
|
|
).format(days=cooldown_days) + "\n\n"
|
|
|
|
keyboard = []
|
|
|
|
if can_request:
|
|
text += texts.t(
|
|
"REFERRAL_WITHDRAWAL_READY",
|
|
"✅ Вы можете запросить вывод средств"
|
|
) + "\n"
|
|
keyboard.append([types.InlineKeyboardButton(
|
|
text=texts.t("REFERRAL_WITHDRAWAL_REQUEST_BUTTON", "📝 Оформить заявку"),
|
|
callback_data="referral_withdrawal_start"
|
|
)])
|
|
else:
|
|
text += f"❌ {reason}\n"
|
|
|
|
keyboard.append([types.InlineKeyboardButton(
|
|
text=texts.BACK,
|
|
callback_data="menu_referrals"
|
|
)])
|
|
|
|
await edit_or_answer_photo(
|
|
callback,
|
|
text,
|
|
types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
|
)
|
|
await callback.answer()
|
|
|
|
|
|
async def start_withdrawal_request(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
db: AsyncSession,
|
|
state: FSMContext
|
|
):
|
|
"""Начинает процесс оформления заявки на вывод."""
|
|
texts = get_texts(db_user.language)
|
|
|
|
# Повторная проверка
|
|
can_request, reason = await referral_withdrawal_service.can_request_withdrawal(db, db_user.id)
|
|
if not can_request:
|
|
await callback.answer(reason, show_alert=True)
|
|
return
|
|
|
|
available = await referral_withdrawal_service.get_available_for_withdrawal(db, db_user.id)
|
|
|
|
# Сохраняем доступный баланс в состоянии
|
|
await state.update_data(available_balance=available)
|
|
await state.set_state(ReferralWithdrawalStates.waiting_for_amount)
|
|
|
|
text = texts.t(
|
|
"REFERRAL_WITHDRAWAL_ENTER_AMOUNT",
|
|
"💸 Введите сумму для вывода в рублях\n\nДоступно: <b>{amount}</b>"
|
|
).format(amount=texts.format_price(available))
|
|
|
|
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(
|
|
text=texts.t("REFERRAL_WITHDRAWAL_ALL", f"Вывести всё ({available / 100:.0f}₽)"),
|
|
callback_data=f"referral_withdrawal_amount_{available}"
|
|
)],
|
|
[types.InlineKeyboardButton(
|
|
text=texts.t("CANCEL", "❌ Отмена"),
|
|
callback_data="referral_withdrawal_cancel"
|
|
)]
|
|
])
|
|
|
|
await edit_or_answer_photo(callback, text, keyboard)
|
|
await callback.answer()
|
|
|
|
|
|
async def process_withdrawal_amount(
|
|
message: types.Message,
|
|
db_user: User,
|
|
db: AsyncSession,
|
|
state: FSMContext
|
|
):
|
|
"""Обрабатывает ввод суммы для вывода."""
|
|
texts = get_texts(db_user.language)
|
|
data = await state.get_data()
|
|
available = data.get("available_balance", 0)
|
|
|
|
try:
|
|
# Парсим сумму (в рублях)
|
|
amount_text = message.text.strip().replace(",", ".").replace("₽", "").replace(" ", "")
|
|
amount_rubles = float(amount_text)
|
|
amount_kopeks = int(amount_rubles * 100)
|
|
|
|
if amount_kopeks <= 0:
|
|
await message.answer(texts.t("REFERRAL_WITHDRAWAL_INVALID_AMOUNT", "❌ Введите положительную сумму"))
|
|
return
|
|
|
|
min_amount = settings.REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS
|
|
if amount_kopeks < min_amount:
|
|
await message.answer(
|
|
texts.t(
|
|
"REFERRAL_WITHDRAWAL_MIN_ERROR",
|
|
"❌ Минимальная сумма: {amount}"
|
|
).format(amount=texts.format_price(min_amount))
|
|
)
|
|
return
|
|
|
|
if amount_kopeks > available:
|
|
await message.answer(
|
|
texts.t(
|
|
"REFERRAL_WITHDRAWAL_INSUFFICIENT",
|
|
"❌ Недостаточно средств. Доступно: {amount}"
|
|
).format(amount=texts.format_price(available))
|
|
)
|
|
return
|
|
|
|
# Сохраняем сумму и переходим к вводу реквизитов
|
|
await state.update_data(withdrawal_amount=amount_kopeks)
|
|
await state.set_state(ReferralWithdrawalStates.waiting_for_payment_details)
|
|
|
|
text = texts.t(
|
|
"REFERRAL_WITHDRAWAL_ENTER_DETAILS",
|
|
"💳 Введите реквизиты для перевода:\n\n"
|
|
"Например:\n"
|
|
"• СБП: +7 999 123-45-67 (Сбербанк)"
|
|
)
|
|
|
|
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(
|
|
text=texts.t("CANCEL", "❌ Отмена"),
|
|
callback_data="referral_withdrawal_cancel"
|
|
)]
|
|
])
|
|
|
|
await message.answer(text, reply_markup=keyboard)
|
|
|
|
except ValueError:
|
|
await message.answer(texts.t("REFERRAL_WITHDRAWAL_INVALID_AMOUNT", "❌ Введите корректную сумму"))
|
|
|
|
|
|
async def process_withdrawal_amount_callback(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
db: AsyncSession,
|
|
state: FSMContext
|
|
):
|
|
"""Обрабатывает выбор суммы для вывода через кнопку."""
|
|
texts = get_texts(db_user.language)
|
|
|
|
# Получаем сумму из callback_data
|
|
amount_kopeks = int(callback.data.split("_")[-1])
|
|
|
|
# Сохраняем сумму и переходим к вводу реквизитов
|
|
await state.update_data(withdrawal_amount=amount_kopeks)
|
|
await state.set_state(ReferralWithdrawalStates.waiting_for_payment_details)
|
|
|
|
text = texts.t(
|
|
"REFERRAL_WITHDRAWAL_ENTER_DETAILS",
|
|
"💳 Введите реквизиты для перевода:\n\n"
|
|
"Например:\n"
|
|
"• СБП: +7 999 123-45-67 (Сбербанк)"
|
|
)
|
|
|
|
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(
|
|
text=texts.t("CANCEL", "❌ Отмена"),
|
|
callback_data="referral_withdrawal_cancel"
|
|
)]
|
|
])
|
|
|
|
await edit_or_answer_photo(callback, text, keyboard)
|
|
await callback.answer()
|
|
|
|
|
|
async def process_payment_details(
|
|
message: types.Message,
|
|
db_user: User,
|
|
db: AsyncSession,
|
|
state: FSMContext
|
|
):
|
|
"""Обрабатывает ввод реквизитов и показывает подтверждение."""
|
|
texts = get_texts(db_user.language)
|
|
data = await state.get_data()
|
|
amount_kopeks = data.get("withdrawal_amount", 0)
|
|
payment_details = message.text.strip()
|
|
|
|
if len(payment_details) < 10:
|
|
await message.answer(
|
|
texts.t("REFERRAL_WITHDRAWAL_DETAILS_TOO_SHORT", "❌ Реквизиты слишком короткие")
|
|
)
|
|
return
|
|
|
|
# Сохраняем реквизиты
|
|
await state.update_data(payment_details=payment_details)
|
|
await state.set_state(ReferralWithdrawalStates.confirming)
|
|
|
|
text = texts.t("REFERRAL_WITHDRAWAL_CONFIRM_TITLE", "📋 <b>Подтверждение заявки</b>") + "\n\n"
|
|
text += texts.t(
|
|
"REFERRAL_WITHDRAWAL_CONFIRM_AMOUNT",
|
|
"💰 Сумма: <b>{amount}</b>"
|
|
).format(amount=texts.format_price(amount_kopeks)) + "\n\n"
|
|
text += texts.t(
|
|
"REFERRAL_WITHDRAWAL_CONFIRM_DETAILS",
|
|
"💳 Реквизиты:\n<code>{details}</code>"
|
|
).format(details=payment_details) + "\n\n"
|
|
text += texts.t(
|
|
"REFERRAL_WITHDRAWAL_CONFIRM_WARNING",
|
|
"⚠️ После отправки заявка будет рассмотрена администрацией"
|
|
)
|
|
|
|
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(
|
|
text=texts.t("REFERRAL_WITHDRAWAL_CONFIRM_BUTTON", "✅ Подтвердить"),
|
|
callback_data="referral_withdrawal_confirm"
|
|
)],
|
|
[types.InlineKeyboardButton(
|
|
text=texts.t("CANCEL", "❌ Отмена"),
|
|
callback_data="referral_withdrawal_cancel"
|
|
)]
|
|
])
|
|
|
|
await message.answer(text, reply_markup=keyboard)
|
|
|
|
|
|
async def confirm_withdrawal_request(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
db: AsyncSession,
|
|
state: FSMContext
|
|
):
|
|
"""Подтверждает и создаёт заявку на вывод."""
|
|
texts = get_texts(db_user.language)
|
|
data = await state.get_data()
|
|
amount_kopeks = data.get("withdrawal_amount", 0)
|
|
payment_details = data.get("payment_details", "")
|
|
|
|
await state.clear()
|
|
|
|
# Создаём заявку
|
|
request, error = await referral_withdrawal_service.create_withdrawal_request(
|
|
db, db_user.id, amount_kopeks, payment_details
|
|
)
|
|
|
|
if error:
|
|
await callback.answer(f"❌ {error}", show_alert=True)
|
|
return
|
|
|
|
# Отправляем уведомление админам
|
|
analysis = json.loads(request.risk_analysis) if request.risk_analysis else {}
|
|
|
|
admin_text = f"""
|
|
🔔 <b>Новая заявка на вывод #{request.id}</b>
|
|
|
|
👤 Пользователь: {db_user.full_name or 'Без имени'}
|
|
🆔 ID: <code>{db_user.telegram_id}</code>
|
|
💰 Сумма: <b>{amount_kopeks / 100:.0f}₽</b>
|
|
|
|
💳 Реквизиты:
|
|
<code>{payment_details}</code>
|
|
|
|
{referral_withdrawal_service.format_analysis_for_admin(analysis)}
|
|
"""
|
|
|
|
admin_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[
|
|
types.InlineKeyboardButton(
|
|
text="✅ Одобрить",
|
|
callback_data=f"admin_withdrawal_approve_{request.id}"
|
|
),
|
|
types.InlineKeyboardButton(
|
|
text="❌ Отклонить",
|
|
callback_data=f"admin_withdrawal_reject_{request.id}"
|
|
)
|
|
],
|
|
[types.InlineKeyboardButton(
|
|
text="👤 Профиль пользователя",
|
|
callback_data=f"admin_user_{db_user.telegram_id}"
|
|
)]
|
|
])
|
|
|
|
try:
|
|
notification_service = AdminNotificationService(callback.bot)
|
|
await notification_service.send_to_admins(
|
|
admin_text,
|
|
keyboard=admin_keyboard
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Ошибка отправки уведомления админам о заявке на вывод: {e}")
|
|
|
|
# Уведомление в топик, если настроено
|
|
topic_id = settings.REFERRAL_WITHDRAWAL_NOTIFICATIONS_TOPIC_ID
|
|
if topic_id and settings.ADMIN_NOTIFICATIONS_CHAT_ID:
|
|
try:
|
|
await callback.bot.send_message(
|
|
chat_id=settings.ADMIN_NOTIFICATIONS_CHAT_ID,
|
|
message_thread_id=topic_id,
|
|
text=admin_text,
|
|
reply_markup=admin_keyboard,
|
|
parse_mode="HTML"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Ошибка отправки уведомления в топик о заявке на вывод: {e}")
|
|
|
|
# Отвечаем пользователю
|
|
text = texts.t(
|
|
"REFERRAL_WITHDRAWAL_SUCCESS",
|
|
"✅ <b>Заявка #{id} создана!</b>\n\n"
|
|
"Сумма: <b>{amount}</b>\n\n"
|
|
"Ваша заявка будет рассмотрена администрацией. "
|
|
"Мы уведомим вас о результате."
|
|
).format(id=request.id, amount=texts.format_price(amount_kopeks))
|
|
|
|
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(
|
|
text=texts.BACK,
|
|
callback_data="menu_referrals"
|
|
)]
|
|
])
|
|
|
|
await edit_or_answer_photo(callback, text, keyboard)
|
|
await callback.answer()
|
|
|
|
|
|
async def cancel_withdrawal_request(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
state: FSMContext
|
|
):
|
|
"""Отменяет процесс создания заявки на вывод."""
|
|
await state.clear()
|
|
texts = get_texts(db_user.language)
|
|
await callback.answer(texts.t("CANCELLED", "Отменено"))
|
|
|
|
# Возвращаем в меню партнёрки
|
|
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
|
[types.InlineKeyboardButton(
|
|
text=texts.BACK,
|
|
callback_data="menu_referrals"
|
|
)]
|
|
])
|
|
await edit_or_answer_photo(
|
|
callback,
|
|
texts.t("REFERRAL_WITHDRAWAL_CANCELLED", "❌ Заявка отменена"),
|
|
keyboard
|
|
)
|
|
|
|
|
|
def register_handlers(dp: Dispatcher):
|
|
|
|
dp.callback_query.register(
|
|
show_referral_info,
|
|
F.data == "menu_referrals"
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
create_invite_message,
|
|
F.data == "referral_create_invite"
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
show_referral_qr,
|
|
F.data == "referral_show_qr"
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
show_detailed_referral_list,
|
|
F.data == "referral_list"
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
show_referral_analytics,
|
|
F.data == "referral_analytics"
|
|
)
|
|
|
|
async def handle_referral_list_page(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
|
page = int(callback.data.split('_')[-1])
|
|
await show_detailed_referral_list(callback, db_user, db, page)
|
|
|
|
dp.callback_query.register(
|
|
handle_referral_list_page,
|
|
F.data.startswith("referral_list_page_")
|
|
)
|
|
|
|
# Хендлеры вывода реферального баланса
|
|
dp.callback_query.register(
|
|
show_withdrawal_info,
|
|
F.data == "referral_withdrawal"
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
start_withdrawal_request,
|
|
F.data == "referral_withdrawal_start"
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
process_withdrawal_amount_callback,
|
|
F.data.startswith("referral_withdrawal_amount_")
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
confirm_withdrawal_request,
|
|
F.data == "referral_withdrawal_confirm"
|
|
)
|
|
|
|
dp.callback_query.register(
|
|
cancel_withdrawal_request,
|
|
F.data == "referral_withdrawal_cancel"
|
|
)
|
|
|
|
# Обработка текстового ввода суммы
|
|
dp.message.register(
|
|
process_withdrawal_amount,
|
|
ReferralWithdrawalStates.waiting_for_amount
|
|
)
|
|
|
|
# Обработка текстового ввода реквизитов
|
|
dp.message.register(
|
|
process_payment_details,
|
|
ReferralWithdrawalStates.waiting_for_payment_details
|
|
)
|