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
}