From ce041758f13afe2a7a75dc35f32eed370a717024 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Sun, 3 Aug 2025 22:27:41 +0300 Subject: [PATCH] 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. --- bot/handlers/admin/promo_codes.py | 131 +++++++++++++++++++++++++++--- bot/handlers/inline_mode.py | 11 +-- bot/handlers/user/referral.py | 7 +- bot/services/referral_service.py | 37 +++++++++ locales/en.json | 13 +-- locales/ru.json | 13 +-- 6 files changed, 183 insertions(+), 29 deletions(-) diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py index 82abd97..a7ffceb 100644 --- a/bot/handlers/admin/promo_codes.py +++ b/bot/handlers/admin/promo_codes.py @@ -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="✏️ Редактирование промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nДни: {days}\nАктивации: {max_act}\nТекущий срок: {current_validity}\n\nВведите количество дней действия промокода от сегодня (1-365) или 0 для неограниченного срока:", + default="✏️ Редактирование промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nДни: {days}\nАктивации: {max_act}\nТекущий срок: {current_validity}\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🎟 Код: {code}\n🎁 Бонусные дни: {days}\n🔢 Максимальные активации: {max_act}\n⏰ Действует до: {validity}", + 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" )) diff --git a/bot/handlers/inline_mode.py b/bot/handlers/inline_mode.py index 93528cb..83dd4fb 100644 --- a/bot/handlers/inline_mode.py +++ b/bot/handlers/inline_mode.py @@ -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 ) diff --git a/bot/handlers/user/referral.py b/bot/handlers/user/referral.py index be6b418..7536370 100644 --- a/bot/handlers/user/referral.py +++ b/bot/handlers/user/referral.py @@ -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) diff --git a/bot/services/referral_service.py b/bot/services/referral_service.py index 30681d8..1be6063 100644 --- a/bot/services/referral_service.py +++ b/bot/services/referral_service.py @@ -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 + } diff --git a/locales/en.json b/locales/en.json index 6c68dcc..e1b5960 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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{referral_link}\n\nBonuses:{bonus_details}", + "referral_program_info_new": "🎁 Referral Program\n\n📊 Your stats:\n👥 Friends invited: {invited_count}\n💳 Purchased subscription: {purchased_count}\n\n🔗 Your link:\n{referral_link}\n\n💰 Invitation bonuses:\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: {inviter_bonus_days} days\n ➢ Friend: {referee_bonus_days} days", "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": "✏️ Editing Promo Code\n\nStep 1 of 4: Promo Code\n\nCurrent code: {current_code}\n\nEnter new promo code (3-30 characters, letters and numbers only) or send current one to keep:", "admin_promo_edit_step2_bonus_days": "✏️ Editing Promo Code\n\nStep 2 of 4: Bonus Days\n\nCode: {code}\nCurrent days: {current_days}\n\nEnter number of bonus days (1-365):", "admin_promo_edit_step3_max_activations": "✏️ Editing Promo Code\n\nStep 3 of 4: Max Activations\n\nCode: {code}\nDays: {days}\nCurrent activations: {current_max}\n\nEnter maximum number of activations (1-10000):", - "admin_promo_edit_step4_validity": "✏️ Editing Promo Code\n\nStep 4 of 4: Validity Period\n\nCode: {code}\nDays: {days}\nActivations: {max_act}\nCurrent validity: {current_validity}\n\nEnter number of days the promo code is valid from today (1-365) or 0 for unlimited:", + "admin_promo_edit_step4_validity": "✏️ Editing Promo Code\n\nStep 4 of 4: Validity Period\n\nCode: {code}\nDays: {days}\nActivations: {max_act}\nCurrent validity: {current_validity}\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: {code}\n🎁 Bonus days: {days}\n🔢 Max activations: {max_act}\n⏰ Valid until: {validity}", "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", diff --git a/locales/ru.json b/locales/ru.json index 574bcc8..7c199c8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -77,7 +77,7 @@ "no_button": "Нет", "trial_cancelled_short": "Активация отменена.", - "referral_program_info_new": "Приглашайте друзей и получайте бонусы!\n\nВаша реф. ссылка:\n{referral_link}\n\nБонусы:{bonus_details}", + "referral_program_info_new": "🎁 Реферальная программа\n\n📊 Твоя статистика:\n👥 Приглашено друзей: {invited_count}\n💳 Купили подписку: {purchased_count}\n\n🔗 Твоя ссылка:\n{referral_link}\n\n💰 Бонусы за приглашения:\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!", "referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: {inviter_bonus_days} дн.\n ➢ Друг: {referee_bonus_days} дн.", "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": "✏️ Редактирование промокода\n\nШаг 1 из 4: Код промокода\n\nТекущий код: {current_code}\n\nВведите новый код промокода (3-30 символов, только буквы и цифры) или отправьте текущий для сохранения:", "admin_promo_edit_step2_bonus_days": "✏️ Редактирование промокода\n\nШаг 2 из 4: Бонусные дни\n\nКод: {code}\nТекущие дни: {current_days}\n\nВведите количество бонусных дней (1-365):", "admin_promo_edit_step3_max_activations": "✏️ Редактирование промокода\n\nШаг 3 из 4: Максимальные активации\n\nКод: {code}\nДни: {days}\nТекущие активации: {current_max}\n\nВведите максимальное количество активаций (1-10000):", - "admin_promo_edit_step4_validity": "✏️ Редактирование промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nДни: {days}\nАктивации: {max_act}\nТекущий срок: {current_validity}\n\nВведите количество дней действия промокода от сегодня (1-365) или 0 для неограниченного срока:", + "admin_promo_edit_step4_validity": "✏️ Редактирование промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nДни: {days}\nАктивации: {max_act}\nТекущий срок: {current_validity}\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🎟 Код: {code}\n🎁 Бонусные дни: {days}\n🔢 Максимальные активации: {max_act}\n⏰ Действует до: {validity}", "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": "Статистика бота",