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": "Статистика бота",