Enhance referral program features and localization updates

- Implemented referral statistics retrieval to provide users with insights on invited friends and subscription purchases.
- Updated referral command handler to include new statistics in the response message, improving user engagement.
- Revised localization files for both English and Russian to reflect changes in the referral program messaging, enhancing clarity and user experience.
This commit is contained in:
machka-pasla
2025-08-03 22:27:41 +03:00
parent b13883c039
commit ce041758f1
6 changed files with 183 additions and 29 deletions
+120 -11
View File
@@ -805,15 +805,35 @@ async def process_promo_edit_max_activations_handler(message: types.Message, sta
await state.update_data(new_max_activations=max_activations)
# Step 4: Ask for validity days
# Step 4: Ask for validity days (with buttons)
current_valid_until = data.get("original_valid_until")
current_validity_str = _("admin_promo_valid_indefinitely", default="Неограниченно")
if current_valid_until:
current_validity_str = current_valid_until.strftime('%Y-%m-%d')
keyboard = InlineKeyboardBuilder()
keyboard.row(
InlineKeyboardButton(
text=_("admin_promo_edit_unlimited_validity", default="♾️ Неограниченно"),
callback_data="promo_edit_unlimited_validity"
)
)
keyboard.row(
InlineKeyboardButton(
text=_("admin_promo_edit_set_validity", default="⏰ Установить срок"),
callback_data="promo_edit_set_validity"
)
)
keyboard.row(
InlineKeyboardButton(
text=_("admin_panel_back_button", default="⬅️ Назад"),
callback_data="admin_panel_back"
)
)
prompt_text = _(
"admin_promo_edit_step4_validity",
default="✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nАктивации: <b>{max_act}</b>\nТекущий срок: <b>{current_validity}</b>\n\nВведите количество дней действия промокода от сегодня (1-365) или 0 для неограниченного срока:",
default="✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nАктивации: <b>{max_act}</b>\nТекущий срок: <b>{current_validity}</b>\n\nВыберите срок действия промокода:",
code=data.get("new_code"),
days=data.get("new_bonus_days"),
max_act=max_activations,
@@ -822,7 +842,7 @@ async def process_promo_edit_max_activations_handler(message: types.Message, sta
await message.answer(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
reply_markup=keyboard.as_markup(),
parse_mode="HTML"
)
await state.set_state(AdminStates.waiting_for_promo_edit_validity_days)
@@ -834,6 +854,98 @@ async def process_promo_edit_max_activations_handler(message: types.Message, sta
))
# Handle edit validity choice buttons
@router.callback_query(F.data == "promo_edit_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_edit_validity_days))
async def process_promo_edit_unlimited_validity(callback: types.CallbackQuery,
state: FSMContext,
i18n_data: dict,
settings: Settings,
session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
await callback.answer("Language service error.", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
data = await state.get_data()
promo_id = data.get("edit_promo_id")
# Update promo code with unlimited validity
update_data = {
"code": data.get("new_code"),
"bonus_days": data.get("new_bonus_days"),
"max_activations": data.get("new_max_activations"),
"valid_until": None, # Unlimited
}
# Show updating message
try:
await callback.message.edit_text(
_("admin_promo_creating", default="⏳ Обновляю промокод..."),
)
except:
await callback.message.answer(
_("admin_promo_creating", default="⏳ Обновляю промокод..."),
)
await callback.answer()
updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
if updated:
await session.commit()
success_text = _(
"admin_promo_updated_success",
default="✅ Промокод обновлен!\n\n🎟 Код: <b>{code}</b>\n🎁 Бонусные дни: <b>{days}</b>\n🔢 Максимальные активации: <b>{max_act}</b>\n⏰ Действует до: <b>{validity}</b>",
code=data.get("new_code"),
days=data.get("new_bonus_days"),
max_act=data.get("new_max_activations"),
validity=_("admin_promo_valid_indefinitely", default="Неограниченно")
)
await callback.message.answer(
success_text,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
parse_mode="HTML"
)
else:
await session.rollback()
await callback.message.answer(_(
"admin_promo_update_failed",
default="❌ Ошибка обновления промокода"
))
await state.clear()
@router.callback_query(F.data == "promo_edit_set_validity", StateFilter(AdminStates.waiting_for_promo_edit_validity_days))
async def process_promo_edit_set_validity(callback: types.CallbackQuery,
state: FSMContext,
i18n_data: dict,
settings: Settings):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
await callback.answer("Language service error.", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
prompt_text = _(
"admin_promo_edit_enter_validity_days",
default="⏰ Введите количество дней действия промокода (1-365):"
)
try:
await callback.message.edit_text(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
)
except Exception as e:
await callback.message.answer(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
)
await callback.answer()
# Step 4: Process edited validity and finalize
@router.message(AdminStates.waiting_for_promo_edit_validity_days, F.text)
async def process_promo_edit_validity_handler(message: types.Message, state: FSMContext,
@@ -851,19 +963,16 @@ async def process_promo_edit_validity_handler(message: types.Message, state: FSM
try:
validity_days = int(message.text.strip())
if not (0 <= validity_days <= 365):
if not (1 <= validity_days <= 365):
await message.answer(_(
"admin_promo_invalid_validity_days",
default="Срок действия должен быть от 0 до 365 дней (0 = неограниченно)"
default="Дни действия должны быть от 1 до 365"
))
return
# Calculate validity date
valid_until_date = None
valid_until_str_display = _("admin_promo_valid_indefinitely", default="Неограниченно")
if validity_days > 0:
valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
# Update promo code
update_data = {
@@ -903,7 +1012,7 @@ async def process_promo_edit_validity_handler(message: types.Message, state: FSM
except ValueError:
await message.answer(_(
"admin_promo_invalid_validity_days",
default="Срок действия должен быть от 0 до 365 дней (0 = неограниченно)"
default="Дни действия должны быть от 1 до 365"
))
+4 -7
View File
@@ -82,14 +82,11 @@ async def create_referral_result(inline_query: InlineQuery, bot: Bot,
user_id = inline_query.from_user.id
referral_link = referral_service.generate_referral_link(bot_username, user_id)
# Create message content
# Create message content (use same text as friend message)
message_text = _(
"inline_referral_message",
default="🚀 Привет! Попробуй этот крутой VPN сервис!\n\n"
"✨ Быстрый и надежный\n"
"🔒 Полная анонимность\n"
"🌍 Серверы по всему миру\n"
"💎 Бесплатный пробный период\n\n{referral_link}",
"referral_friend_message",
default="🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n"
"🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}",
referral_link=referral_link
)
+6 -1
View File
@@ -83,9 +83,14 @@ async def referral_command_handler(event: Union[types.Message,
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
"referral_no_bonuses_configured")
# Get referral statistics
referral_stats = await referral_service.get_referral_stats(session, inviter_user_id)
text = _("referral_program_info_new",
referral_link=referral_link,
bonus_details=bonus_details_str)
bonus_details=bonus_details_str,
invited_count=referral_stats["invited_count"],
purchased_count=referral_stats["purchased_count"])
from bot.keyboards.inline.user_keyboards import get_referral_link_keyboard
reply_markup_val = get_referral_link_keyboard(current_lang, i18n)
+37
View File
@@ -1,5 +1,6 @@
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from typing import Optional, Dict, Any
from aiogram import Bot
from datetime import datetime, timezone, timedelta
@@ -219,3 +220,39 @@ class ReferralService:
def generate_referral_link(self, bot_username: str,
inviter_user_id: int) -> str:
return f"https://t.me/{bot_username}?start=ref_{inviter_user_id}"
async def get_referral_stats(self, session: AsyncSession, user_id: int) -> dict:
"""Get referral statistics for a user"""
from db.dal import user_dal, payment_dal
try:
# Count total invited users (referrals)
invited_count_result = await session.execute(
text("SELECT COUNT(*) FROM users WHERE referred_by_id = :user_id"),
{"user_id": user_id}
)
invited_count = invited_count_result.scalar() or 0
# Count users who made successful payments (purchased subscription)
purchased_count_result = await session.execute(
text("""
SELECT COUNT(DISTINCT u.user_id)
FROM users u
JOIN payments p ON u.user_id = p.user_id
WHERE u.referred_by_id = :user_id
AND p.status = 'succeeded'
"""),
{"user_id": user_id}
)
purchased_count = purchased_count_result.scalar() or 0
return {
"invited_count": invited_count,
"purchased_count": purchased_count
}
except Exception as e:
logging.error(f"Error getting referral stats for user {user_id}: {e}")
return {
"invited_count": 0,
"purchased_count": 0
}
+8 -5
View File
@@ -77,7 +77,7 @@
"no_button": "No",
"trial_cancelled_short": "Activation cancelled.",
"referral_program_info_new": "Invite friends and earn bonuses!\n\nYour referral link:\n<code>{referral_link}</code>\n\n<b>Bonuses:</b>{bonus_details}",
"referral_program_info_new": "🎁 <b>Referral Program</b>\n\n📊 <b>Your stats:</b>\n👥 Friends invited: <b>{invited_count}</b>\n💳 Purchased subscription: <b>{purchased_count}</b>\n\n🔗 Your link:\n<code>{referral_link}</code>\n\n💰 <b>Invitation bonuses:</b>\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!",
"referral_bonus_per_period": "\n\n🎁 For a friend's {months}-month subscription:\n ➢ You: <b>{inviter_bonus_days} days</b>\n ➢ Friend: <b>{referee_bonus_days} days</b>",
"no_bonus_days": "0",
"referral_no_bonuses_configured": "\nNo referral bonuses configured.",
@@ -271,7 +271,7 @@
"admin_user_logs_error": "❌ Error loading user actions",
"admin_direct_message_signature": "\n\n---\n💬 Message from administrator",
"inline_referral_message": "🚀 Hey! Try this awesome VPN service!\n\n✨ Fast and reliable\n🔒 Complete anonymity\n🌍 Servers worldwide\n💎 Free trial period\n\nCheck it out: {referral_link}",
"inline_referral_title": "🎁 Invite a Friend",
"inline_referral_description": "Share referral link to get bonuses",
@@ -311,10 +311,13 @@
"admin_promo_edit_step1_code": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 1 of 4:</b> Promo Code\n\nCurrent code: <b>{current_code}</b>\n\nEnter new promo code (3-30 characters, letters and numbers only) or send current one to keep:",
"admin_promo_edit_step2_bonus_days": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 2 of 4:</b> Bonus Days\n\nCode: <b>{code}</b>\nCurrent days: <b>{current_days}</b>\n\nEnter number of bonus days (1-365):",
"admin_promo_edit_step3_max_activations": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 3 of 4:</b> Max Activations\n\nCode: <b>{code}</b>\nDays: <b>{days}</b>\nCurrent activations: <b>{current_max}</b>\n\nEnter maximum number of activations (1-10000):",
"admin_promo_edit_step4_validity": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 4 of 4:</b> Validity Period\n\nCode: <b>{code}</b>\nDays: <b>{days}</b>\nActivations: <b>{max_act}</b>\nCurrent validity: <b>{current_validity}</b>\n\nEnter number of days the promo code is valid from today (1-365) or 0 for unlimited:",
"admin_promo_edit_step4_validity": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 4 of 4:</b> Validity Period\n\nCode: <b>{code}</b>\nDays: <b>{days}</b>\nActivations: <b>{max_act}</b>\nCurrent validity: <b>{current_validity}</b>\n\nChoose the validity period for the promo code:",
"admin_promo_edit_unlimited_validity": "♾️ Unlimited",
"admin_promo_edit_set_validity": "⏰ Set Period",
"admin_promo_edit_enter_validity_days": "⏰ Enter the number of validity days for the promo code (1-365):",
"admin_promo_updated_success": "✅ Promo code updated!\n\n🎟 Code: <b>{code}</b>\n🎁 Bonus days: <b>{days}</b>\n🔢 Max activations: <b>{max_act}</b>\n⏰ Valid until: <b>{validity}</b>",
"admin_promo_update_failed": "❌ Error updating promo code",
"admin_promo_invalid_validity_days": "❌ Validity days must be from 0 to 365 days (0 = unlimited)",
"admin_promo_invalid_validity_days": "❌ Validity days must be from 1 to 365",
"admin_logs_export_csv": "📄 Export to CSV",
"admin_logs_csv_export_started": "📄 Starting log export to CSV...",
@@ -452,7 +455,7 @@
"admin_panel_stats_fetch_error": "Error fetching panel data",
"admin_panel_stats_error_details": "Details",
"inline_referral_message": "🎁 Get bonuses for friends!\n\nConnect friends to fast VPN and get bonus days for each new user.\n\n💰 Your referral link:\n{referral_link}",
"inline_referral_message": "🚀 Hey! Try this VPN - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\n{referral_link}",
"inline_referral_title": "Referral link",
"inline_referral_description": "Share referral link to get bonuses",
"inline_stats_title": "Bot statistics",
+8 -5
View File
@@ -77,7 +77,7 @@
"no_button": "Нет",
"trial_cancelled_short": "Активация отменена.",
"referral_program_info_new": "Приглашайте друзей и получайте бонусы!\n\nВаша реф. ссылка:\n<code>{referral_link}</code>\n\n<b>Бонусы:</b>{bonus_details}",
"referral_program_info_new": "🎁 <b>Реферальная программа</b>\n\n📊 <b>Твоя статистика:</b>\n👥 Приглашено друзей: <b>{invited_count}</b>\n💳 Купили подписку: <b>{purchased_count}</b>\n\n🔗 Твоя ссылка:\n<code>{referral_link}</code>\n\n💰 <b>Бонусы за приглашения:</b>\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!",
"referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: <b>{inviter_bonus_days} дн.</b>\n ➢ Друг: <b>{referee_bonus_days} дн.</b>",
"no_bonus_days": "0",
"referral_no_bonuses_configured": "\nРеферальные бонусы не настроены.",
@@ -271,7 +271,7 @@
"admin_user_logs_error": "❌ Ошибка загрузки действий пользователя",
"admin_direct_message_signature": "\n\n---\n💬 Сообщение от администратора",
"inline_referral_message": "🚀 Привет! Попробуй этот крутой VPN сервис!\n\n✨ Быстрый и надежный\n🔒 Полная анонимность\n🌍 Серверы по всему миру\n💎 Бесплатный пробный период\n\nПереходи по ссылке: {referral_link}",
"inline_referral_title": "🎁 Пригласить друга",
"inline_referral_description": "Поделиться реферальной ссылкой для получения бонусов",
@@ -312,10 +312,13 @@
"admin_promo_edit_step1_code": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 1 из 4:</b> Код промокода\n\nТекущий код: <b>{current_code}</b>\n\nВведите новый код промокода (3-30 символов, только буквы и цифры) или отправьте текущий для сохранения:",
"admin_promo_edit_step2_bonus_days": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКод: <b>{code}</b>\nТекущие дни: <b>{current_days}</b>\n\nВведите количество бонусных дней (1-365):",
"admin_promo_edit_step3_max_activations": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nТекущие активации: <b>{current_max}</b>\n\nВведите максимальное количество активаций (1-10000):",
"admin_promo_edit_step4_validity": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nАктивации: <b>{max_act}</b>\nТекущий срок: <b>{current_validity}</b>\n\nВведите количество дней действия промокода от сегодня (1-365) или 0 для неограниченного срока:",
"admin_promo_edit_step4_validity": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nАктивации: <b>{max_act}</b>\nТекущий срок: <b>{current_validity}</b>\n\nВыберите срок действия промокода:",
"admin_promo_edit_unlimited_validity": "♾️ Неограниченно",
"admin_promo_edit_set_validity": "⏰ Установить срок",
"admin_promo_edit_enter_validity_days": "⏰ Введите количество дней действия промокода (1-365):",
"admin_promo_updated_success": "✅ Промокод обновлен!\n\n🎟 Код: <b>{code}</b>\n🎁 Бонусные дни: <b>{days}</b>\n🔢 Максимальные активации: <b>{max_act}</b>\n⏰ Действует до: <b>{validity}</b>",
"admin_promo_update_failed": "❌ Ошибка обновления промокода",
"admin_promo_invalid_validity_days": "❌ Срок действия должен быть от 0 до 365 дней (0 = неограниченно)",
"admin_promo_invalid_validity_days": "❌ Дни действия должны быть от 1 до 365",
"admin_logs_export_csv": "📄 Экспорт в CSV",
"admin_logs_csv_export_started": "📄 Начинаю экспорт логов в CSV...",
@@ -453,7 +456,7 @@
"admin_panel_stats_fetch_error": "Ошибка получения данных с панели",
"admin_panel_stats_error_details": "Детали",
"inline_referral_message": "🎁 Получи бонусы за друзей!\n\nПодключай друзей к быстрому VPN и получай бонусные дни за каждого нового пользователя.\n\n💰 Твоя реферальная ссылка:\n{referral_link}",
"inline_referral_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}",
"inline_referral_title": "Реферальная ссылка",
"inline_referral_description": "Поделиться реферальной ссылкой для получения бонусов",
"inline_stats_title": "Статистика бота",