diff --git a/.gitignore b/.gitignore
index 9bb24585..9f6d9e30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ docker-compose.override.yml
!requirements.txt
!docs/
!docs/**
+!migrations/
+!migrations/**
# Разрешаем папку app/ и все её содержимое рекурсивно
!app/
diff --git a/app/database/models.py b/app/database/models.py
index 1b7a7123..f8e9f719 100644
--- a/app/database/models.py
+++ b/app/database/models.py
@@ -595,6 +595,7 @@ class User(Base):
lifetime_used_traffic_bytes = Column(BigInteger, default=0)
auto_promo_group_assigned = Column(Boolean, nullable=False, default=False)
auto_promo_group_threshold_kopeks = Column(BigInteger, nullable=False, default=0)
+ referral_commission_percent = Column(Integer, nullable=True)
promo_offer_discount_percent = Column(Integer, nullable=False, default=0)
promo_offer_discount_source = Column(String(100), nullable=True)
promo_offer_discount_expires_at = Column(DateTime, nullable=True)
diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py
index e0caeb11..6bf30aff 100644
--- a/app/database/universal_migration.py
+++ b/app/database/universal_migration.py
@@ -2742,6 +2742,35 @@ async def fix_foreign_keys_for_user_deletion():
logger.error(f"Ошибка обновления внешних ключей: {e}")
return False
+async def add_referral_commission_percent_column() -> bool:
+ column_exists = await check_column_exists('users', 'referral_commission_percent')
+ if column_exists:
+ logger.info("ℹ️ Колонка referral_commission_percent уже существует")
+ return True
+
+ try:
+ async with engine.begin() as conn:
+ db_type = await get_database_type()
+
+ if db_type == 'sqlite':
+ alter_sql = "ALTER TABLE users ADD COLUMN referral_commission_percent INTEGER NULL"
+ elif db_type == 'postgresql':
+ alter_sql = "ALTER TABLE users ADD COLUMN referral_commission_percent INTEGER NULL"
+ elif db_type == 'mysql':
+ alter_sql = "ALTER TABLE users ADD COLUMN referral_commission_percent INT NULL"
+ else:
+ logger.error(f"Неподдерживаемый тип БД для добавления referral_commission_percent: {db_type}")
+ return False
+
+ await conn.execute(text(alter_sql))
+ logger.info("✅ Добавлена колонка referral_commission_percent в таблицу users")
+ return True
+
+ except Exception as error:
+ logger.error(f"Ошибка добавления referral_commission_percent: {error}")
+ return False
+
+
async def add_referral_system_columns():
logger.info("=== МИГРАЦИЯ РЕФЕРАЛЬНОЙ СИСТЕМЫ ===")
@@ -3809,6 +3838,12 @@ async def run_universal_migration():
if not referral_migration_success:
logger.warning("⚠️ Проблемы с миграцией реферальной системы")
+ commission_column_ready = await add_referral_commission_percent_column()
+ if commission_column_ready:
+ logger.info("✅ Колонка referral_commission_percent готова")
+ else:
+ logger.warning("⚠️ Проблемы с колонкой referral_commission_percent")
+
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ SYSTEM_SETTINGS ===")
system_settings_ready = await create_system_settings_table()
if system_settings_ready:
@@ -4223,6 +4258,7 @@ async def check_migration_status():
"users_promo_offer_discount_percent_column": False,
"users_promo_offer_discount_source_column": False,
"users_promo_offer_discount_expires_column": False,
+ "users_referral_commission_percent_column": False,
"subscription_crypto_link_column": False,
"discount_offers_table": False,
"discount_offers_effect_column": False,
@@ -4265,6 +4301,7 @@ async def check_migration_status():
status["users_promo_offer_discount_percent_column"] = await check_column_exists('users', 'promo_offer_discount_percent')
status["users_promo_offer_discount_source_column"] = await check_column_exists('users', 'promo_offer_discount_source')
status["users_promo_offer_discount_expires_column"] = await check_column_exists('users', 'promo_offer_discount_expires_at')
+ status["users_referral_commission_percent_column"] = await check_column_exists('users', 'referral_commission_percent')
status["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link')
media_fields_exist = (
@@ -4312,6 +4349,7 @@ async def check_migration_status():
"users_promo_offer_discount_percent_column": "Колонка процента промо-скидки у пользователей",
"users_promo_offer_discount_source_column": "Колонка источника промо-скидки у пользователей",
"users_promo_offer_discount_expires_column": "Колонка срока действия промо-скидки у пользователей",
+ "users_referral_commission_percent_column": "Колонка процента реферальной комиссии у пользователей",
"subscription_crypto_link_column": "Колонка subscription_crypto_link в subscriptions",
"discount_offers_table": "Таблица discount_offers",
"discount_offers_effect_column": "Колонка effect_type в discount_offers",
diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py
index b0ddd157..d25499e6 100644
--- a/app/handlers/admin/users.py
+++ b/app/handlers/admin/users.py
@@ -32,6 +32,7 @@ from app.services.admin_notification_service import AdminNotificationService
from app.database.crud.promo_group import get_promo_groups_with_counts
from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_time_ago
+from app.utils.user_utils import get_effective_referral_commission_percent
from app.services.remnawave_service import RemnaWaveService
from app.external.remnawave_api import TrafficLimitStrategy
from app.database.crud.server_squad import (
@@ -1536,6 +1537,9 @@ async def _build_user_referrals_view(
referrals = await get_referrals(db, user_id)
+ effective_percent = get_effective_referral_commission_percent(user)
+ default_percent = settings.REFERRAL_COMMISSION_PERCENT
+
header = texts.t(
"ADMIN_USER_REFERRALS_TITLE",
"🤝 Рефералы пользователя",
@@ -1551,6 +1555,24 @@ async def _build_user_referrals_view(
lines: List[str] = [header, summary]
+ if user.referral_commission_percent is None:
+ lines.append(
+ texts.t(
+ "ADMIN_USER_REFERRAL_COMMISSION_DEFAULT",
+ "• Процент комиссии: {percent}% (стандартное значение)",
+ ).format(percent=effective_percent)
+ )
+ else:
+ lines.append(
+ texts.t(
+ "ADMIN_USER_REFERRAL_COMMISSION_CUSTOM",
+ "• Индивидуальный процент: {percent}% (стандарт: {default_percent}%)",
+ ).format(
+ percent=user.referral_commission_percent,
+ default_percent=default_percent,
+ )
+ )
+
if referrals:
lines.append(
texts.t(
@@ -1604,6 +1626,15 @@ async def _build_user_referrals_view(
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text=texts.t(
+ "ADMIN_USER_REFERRAL_COMMISSION_EDIT_BUTTON",
+ "📈 Изменить процент",
+ ),
+ callback_data=f"admin_user_referral_percent_{user_id}",
+ )
+ ],
[
InlineKeyboardButton(
text=texts.t(
@@ -1636,12 +1667,12 @@ async def show_user_referrals(
user_id = int(callback.data.split('_')[-1])
current_state = await state.get_state()
- if current_state == AdminStates.editing_user_referrals:
+ if current_state in {AdminStates.editing_user_referrals, AdminStates.editing_user_referral_percent}:
data = await state.get_data()
preserved_data = {
key: value
for key, value in data.items()
- if key not in {"editing_referrals_user_id", "referrals_message_id"}
+ if key not in {"editing_referrals_user_id", "referrals_message_id", "editing_referral_percent_user_id"}
}
await state.clear()
if preserved_data:
@@ -1661,6 +1692,256 @@ async def show_user_referrals(
await callback.answer()
+@admin_required
+@error_handler
+async def start_edit_referral_percent(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ user_id = int(callback.data.split('_')[-1])
+
+ user = await get_user_by_id(db, user_id)
+ if not user:
+ await callback.answer("❌ Пользователь не найден", show_alert=True)
+ return
+
+ texts = get_texts(db_user.language)
+
+ effective_percent = get_effective_referral_commission_percent(user)
+ default_percent = settings.REFERRAL_COMMISSION_PERCENT
+
+ prompt = texts.t(
+ "ADMIN_USER_REFERRAL_COMMISSION_PROMPT",
+ (
+ "📈 Индивидуальный процент реферальной комиссии\n\n"
+ "Текущее значение: {current}%\n"
+ "Стандартное значение: {default}%\n\n"
+ "Отправьте новое значение от 0 до 100 или слово 'стандарт' для сброса."
+ ),
+ ).format(current=effective_percent, default=default_percent)
+
+ keyboard = InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text="5%",
+ callback_data=f"admin_user_referral_percent_set_{user_id}_5",
+ ),
+ InlineKeyboardButton(
+ text="10%",
+ callback_data=f"admin_user_referral_percent_set_{user_id}_10",
+ ),
+ ],
+ [
+ InlineKeyboardButton(
+ text="15%",
+ callback_data=f"admin_user_referral_percent_set_{user_id}_15",
+ ),
+ InlineKeyboardButton(
+ text="20%",
+ callback_data=f"admin_user_referral_percent_set_{user_id}_20",
+ ),
+ ],
+ [
+ InlineKeyboardButton(
+ text=texts.t(
+ "ADMIN_USER_REFERRAL_COMMISSION_RESET_BUTTON",
+ "♻️ Сбросить на стандартный",
+ ),
+ callback_data=f"admin_user_referral_percent_reset_{user_id}",
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text=texts.BACK,
+ callback_data=f"admin_user_referrals_{user_id}",
+ )
+ ],
+ ]
+ )
+
+ await state.update_data(editing_referral_percent_user_id=user_id)
+ await state.set_state(AdminStates.editing_user_referral_percent)
+
+ await callback.message.edit_text(
+ prompt,
+ reply_markup=keyboard,
+ )
+ await callback.answer()
+
+
+async def _update_referral_commission_percent(
+ db: AsyncSession,
+ user_id: int,
+ percent: Optional[int],
+ admin_id: int,
+) -> Tuple[bool, Optional[int]]:
+ try:
+ user = await get_user_by_id(db, user_id)
+ if not user:
+ return False, None
+
+ user.referral_commission_percent = percent
+ user.updated_at = datetime.utcnow()
+
+ await db.commit()
+
+ effective = get_effective_referral_commission_percent(user)
+
+ logger.info(
+ "Админ %s обновил реферальный процент пользователя %s: %s",
+ admin_id,
+ user_id,
+ percent,
+ )
+
+ return True, effective
+ except Exception as e:
+ logger.error(
+ "Ошибка обновления реферального процента пользователя %s: %s",
+ user_id,
+ e,
+ )
+ try:
+ await db.rollback()
+ except Exception as rollback_error:
+ logger.error("Ошибка отката транзакции: %s", rollback_error)
+ return False, None
+
+
+async def _render_referrals_after_update(
+ callback: types.CallbackQuery,
+ db: AsyncSession,
+ db_user: User,
+ user_id: int,
+ success_message: str,
+):
+ view = await _build_user_referrals_view(db, db_user.language, user_id)
+ if view:
+ text, keyboard = view
+ text = f"{success_message}\n\n" + text
+ await callback.message.edit_text(text, reply_markup=keyboard)
+ else:
+ await callback.message.edit_text(success_message)
+
+
+@admin_required
+@error_handler
+async def set_referral_percent_button(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+):
+ parts = callback.data.split('_')
+
+ if "reset" in parts:
+ user_id = int(parts[-1])
+ percent_value: Optional[int] = None
+ else:
+ user_id = int(parts[-2])
+ percent_value = int(parts[-1])
+
+ texts = get_texts(db_user.language)
+
+ success, effective_percent = await _update_referral_commission_percent(
+ db,
+ user_id,
+ percent_value,
+ db_user.id,
+ )
+
+ if not success:
+ await callback.answer("❌ Не удалось обновить процент", show_alert=True)
+ return
+
+ await state.clear()
+
+ success_message = texts.t(
+ "ADMIN_USER_REFERRAL_COMMISSION_UPDATED",
+ "✅ Процент обновлён: {percent}%",
+ ).format(percent=effective_percent)
+
+ await _render_referrals_after_update(callback, db, db_user, user_id, success_message)
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_referral_percent_input(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ user_id = data.get("editing_referral_percent_user_id")
+
+ if not user_id:
+ await message.answer("❌ Не удалось определить пользователя")
+ return
+
+ raw_text = message.text.strip()
+ normalized = raw_text.lower()
+
+ percent_value: Optional[int]
+
+ if normalized in {"стандарт", "standard", "default"}:
+ percent_value = None
+ else:
+ normalized_number = raw_text.replace(',', '.').strip()
+ try:
+ percent_float = float(normalized_number)
+ except (TypeError, ValueError):
+ await message.answer(
+ get_texts(db_user.language).t(
+ "ADMIN_USER_REFERRAL_COMMISSION_INVALID",
+ "❌ Введите число от 0 до 100 или слово 'стандарт'",
+ )
+ )
+ return
+
+ percent_value = int(round(percent_float))
+
+ if percent_value < 0 or percent_value > 100:
+ await message.answer(
+ get_texts(db_user.language).t(
+ "ADMIN_USER_REFERRAL_COMMISSION_INVALID",
+ "❌ Введите число от 0 до 100 или слово 'стандарт'",
+ )
+ )
+ return
+
+ texts = get_texts(db_user.language)
+
+ success, effective_percent = await _update_referral_commission_percent(
+ db,
+ int(user_id),
+ percent_value,
+ db_user.id,
+ )
+
+ if not success:
+ await message.answer("❌ Не удалось обновить процент")
+ return
+
+ await state.clear()
+
+ success_message = texts.t(
+ "ADMIN_USER_REFERRAL_COMMISSION_UPDATED",
+ "✅ Процент обновлён: {percent}%",
+ ).format(percent=effective_percent)
+
+ view = await _build_user_referrals_view(db, db_user.language, int(user_id))
+ if view:
+ text, keyboard = view
+ await message.answer(f"{success_message}\n\n{text}", reply_markup=keyboard)
+ else:
+ await message.answer(success_message)
+
+
@admin_required
@error_handler
async def start_edit_user_referrals(
@@ -4621,6 +4902,24 @@ def register_handlers(dp: Dispatcher):
F.data.startswith("admin_user_referrals_") & ~F.data.contains("_edit")
)
+ dp.callback_query.register(
+ start_edit_referral_percent,
+ F.data.startswith("admin_user_referral_percent_")
+ & ~F.data.contains("_set_")
+ & ~F.data.contains("_reset")
+ )
+
+ dp.callback_query.register(
+ set_referral_percent_button,
+ F.data.startswith("admin_user_referral_percent_set_")
+ | F.data.startswith("admin_user_referral_percent_reset_")
+ )
+
+ dp.message.register(
+ process_referral_percent_input,
+ AdminStates.editing_user_referral_percent,
+ )
+
dp.callback_query.register(
start_edit_user_referrals,
F.data.startswith("admin_user_referrals_edit_")
diff --git a/app/handlers/referral.py b/app/handlers/referral.py
index fe3322a8..e2351719 100644
--- a/app/handlers/referral.py
+++ b/app/handlers/referral.py
@@ -14,6 +14,7 @@ from app.localization.texts import get_texts
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,
)
@@ -86,7 +87,7 @@ async def show_referral_info(
+ texts.t(
"REFERRAL_REWARD_COMMISSION",
"• Комиссия с каждого пополнения реферала: {percent}%",
- ).format(percent=settings.REFERRAL_COMMISSION_PERCENT)
+ ).format(percent=get_effective_referral_commission_percent(db_user))
+ "\n\n"
+ texts.t("REFERRAL_LINK_TITLE", "🔗 Ваша реферальная ссылка:")
+ f"\n{referral_link}\n\n"
diff --git a/app/handlers/simple_subscription.py b/app/handlers/simple_subscription.py
index 176d6521..1d9cccab 100644
--- a/app/handlers/simple_subscription.py
+++ b/app/handlers/simple_subscription.py
@@ -134,8 +134,11 @@ async def start_simple_subscription_purchase(
if show_devices:
message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
+ traffic_limit_gb = subscription_params["traffic_limit_gb"]
+ traffic_label = "Безлимит" if traffic_limit_gb == 0 else f"{traffic_limit_gb} ГБ"
+
message_lines.extend([
- f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
+ f"📊 Трафик: {traffic_label}",
f"🌍 Сервер: {server_label}",
"",
f"💰 Стоимость: {settings.format_price(price_kopeks)}",
@@ -523,8 +526,11 @@ async def handle_simple_subscription_pay_with_balance(
if show_devices:
success_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
+ success_traffic_gb = subscription_params["traffic_limit_gb"]
+ success_traffic_label = "Безлимит" if success_traffic_gb == 0 else f"{success_traffic_gb} ГБ"
+
success_lines.extend([
- f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
+ f"📊 Трафик: {success_traffic_label}",
f"🌍 Сервер: {server_label}",
"",
f"💰 Списано с баланса: {settings.format_price(price_kopeks)}",
@@ -721,8 +727,11 @@ async def handle_simple_subscription_other_payment_methods(
if show_devices:
message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
+ payment_traffic_gb = subscription_params["traffic_limit_gb"]
+ payment_traffic_label = "Безлимит" if payment_traffic_gb == 0 else f"{payment_traffic_gb} ГБ"
+
message_lines.extend([
- f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
+ f"📊 Трафик: {payment_traffic_label}",
f"🌍 Сервер: {server_label}",
"",
f"💰 Стоимость: {settings.format_price(price_kopeks)}",
@@ -826,6 +835,9 @@ async def handle_simple_subscription_payment_method(
stars_count = settings.rubles_to_stars(settings.kopeks_to_rubles(price_kopeks))
+ stars_traffic_gb = subscription_params["traffic_limit_gb"]
+ stars_traffic_label = "Безлимит" if stars_traffic_gb == 0 else f"{stars_traffic_gb} ГБ"
+
await callback.bot.send_invoice(
chat_id=callback.from_user.id,
title=f"Подписка на {subscription_params['period_days']} дней",
@@ -833,7 +845,7 @@ async def handle_simple_subscription_payment_method(
f"Простая покупка подписки\n"
f"Период: {subscription_params['period_days']} дней\n"
f"Устройства: {subscription_params['device_limit']}\n"
- f"Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}"
+ f"Трафик: {stars_traffic_label}"
),
payload=(
f"simple_sub_{db_user.id}_{order.id}_{subscription_params['period_days']}"
@@ -977,8 +989,11 @@ async def handle_simple_subscription_payment_method(
if show_devices:
message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
+ yookassa_traffic_gb = subscription_params["traffic_limit_gb"]
+ yookassa_traffic_label = "Безлимит" if yookassa_traffic_gb == 0 else f"{yookassa_traffic_gb} ГБ"
+
message_lines.extend([
- f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
+ f"📊 Трафик: {yookassa_traffic_label}",
f"💰 Сумма: {settings.format_price(price_kopeks)}",
f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...",
"",
@@ -2220,8 +2235,11 @@ async def confirm_simple_subscription_purchase(
if show_devices:
success_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
+ success_traffic_gb = subscription_params["traffic_limit_gb"]
+ success_traffic_label = "Безлимит" if success_traffic_gb == 0 else f"{success_traffic_gb} ГБ"
+
success_lines.extend([
- f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
+ f"📊 Трафик: {success_traffic_label}",
f"🌍 Сервер: {server_label}",
"",
f"💰 Списано с баланса: {settings.format_price(price_kopeks)}",
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index b0d6f203..34067554 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -731,6 +731,22 @@
"ADMIN_USER_PROMO_GROUP_ALREADY": "ℹ️ The user is already in this promo group.",
"ADMIN_USER_PROMO_GROUP_BACK": "⬅️ Back to user",
"ADMIN_USER_PROMO_GROUP_BUTTON": "👥 Promo group",
+ "ADMIN_USER_REFERRALS_BUTTON": "🤝 Referrals",
+ "ADMIN_USER_REFERRALS_TITLE": "🤝 User referrals",
+ "ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: {telegram_id})\n👥 Total referrals: {count}",
+ "ADMIN_USER_REFERRAL_COMMISSION_DEFAULT": "• Commission percent: {percent}% (default)",
+ "ADMIN_USER_REFERRAL_COMMISSION_CUSTOM": "• Custom percent: {percent}% (default: {default_percent}%)",
+ "ADMIN_USER_REFERRAL_COMMISSION_EDIT_BUTTON": "📈 Change percent",
+ "ADMIN_USER_REFERRAL_COMMISSION_PROMPT": "📈 Custom referral commission\n\nCurrent value: {current}%\nDefault value: {default}%\n\nSend a value from 0 to 100 or the word 'standard' to reset.",
+ "ADMIN_USER_REFERRAL_COMMISSION_RESET_BUTTON": "♻️ Reset to default",
+ "ADMIN_USER_REFERRAL_COMMISSION_UPDATED": "✅ Percent updated: {percent}%",
+ "ADMIN_USER_REFERRAL_COMMISSION_INVALID": "❌ Enter a number from 0 to 100 or the word 'standard'",
+ "ADMIN_USER_REFERRALS_LIST_HEADER": "List of referrals:",
+ "ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: {telegram_id}{username_part})",
+ "ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … and {count} more referrals",
+ "ADMIN_USER_REFERRALS_EMPTY": "No referrals yet.",
+ "ADMIN_USER_REFERRALS_EDIT_HINT": "✏️ To change the list, tap “✏️ Edit” below.",
+ "ADMIN_USER_REFERRALS_EDIT_BUTTON": "✏️ Edit",
"ADMIN_USER_PROMO_GROUP_CURRENT": "Current group: {name}",
"ADMIN_USER_PROMO_GROUP_CURRENT_NONE": "Current group: not assigned",
"ADMIN_USER_PROMO_GROUP_DISCOUNTS": "Discounts — servers: {servers}%, traffic: {traffic}%, devices: {devices}%",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index 6af92523..3519200a 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -734,6 +734,13 @@
"ADMIN_USER_REFERRALS_BUTTON": "🤝 Рефералы",
"ADMIN_USER_REFERRALS_TITLE": "🤝 Рефералы пользователя",
"ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: {telegram_id})\n👥 Всего рефералов: {count}",
+ "ADMIN_USER_REFERRAL_COMMISSION_DEFAULT": "• Процент комиссии: {percent}% (стандартное значение)",
+ "ADMIN_USER_REFERRAL_COMMISSION_CUSTOM": "• Индивидуальный процент: {percent}% (стандарт: {default_percent}%)",
+ "ADMIN_USER_REFERRAL_COMMISSION_EDIT_BUTTON": "📈 Изменить процент",
+ "ADMIN_USER_REFERRAL_COMMISSION_PROMPT": "📈 Индивидуальный процент реферальной комиссии\n\nТекущее значение: {current}%\nСтандартное значение: {default}%\n\nОтправьте новое значение от 0 до 100 или слово 'стандарт' для сброса.",
+ "ADMIN_USER_REFERRAL_COMMISSION_RESET_BUTTON": "♻️ Сбросить на стандартный",
+ "ADMIN_USER_REFERRAL_COMMISSION_UPDATED": "✅ Процент обновлён: {percent}%",
+ "ADMIN_USER_REFERRAL_COMMISSION_INVALID": "❌ Введите число от 0 до 100 или слово 'стандарт'",
"ADMIN_USER_REFERRALS_LIST_HEADER": "Список рефералов:",
"ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: {telegram_id}{username_part})",
"ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … и ещё {count} рефералов",
diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json
index 68897290..f0b24625 100644
--- a/app/localization/locales/ua.json
+++ b/app/localization/locales/ua.json
@@ -730,12 +730,19 @@
"ADMIN_USER_PROMO_GROUP_ALREADY": "ℹ️ Користувач вже перебуває у цій промогрупі.",
"ADMIN_USER_PROMO_GROUP_BACK": "⬅️ До користувача",
"ADMIN_USER_PROMO_GROUP_BUTTON": "👥 Промогрупа",
- "ADMIN_USER_REFERRALS_BUTTON": "🤝 Реферали",
- "ADMIN_USER_REFERRALS_TITLE": "🤝 Реферали користувача",
- "ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: {telegram_id})\n👥 Всього рефералів: {count}",
- "ADMIN_USER_REFERRALS_LIST_HEADER": "Список рефералів:",
- "ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: {telegram_id}{username_part})",
- "ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … і ще {count} рефералів",
+ "ADMIN_USER_REFERRALS_BUTTON": "🤝 Реферали",
+ "ADMIN_USER_REFERRALS_TITLE": "🤝 Реферали користувача",
+ "ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: {telegram_id})\n👥 Всього рефералів: {count}",
+ "ADMIN_USER_REFERRAL_COMMISSION_DEFAULT": "• Відсоток комісії: {percent}% (стандартне значення)",
+ "ADMIN_USER_REFERRAL_COMMISSION_CUSTOM": "• Індивідуальний відсоток: {percent}% (стандарт: {default_percent}%)",
+ "ADMIN_USER_REFERRAL_COMMISSION_EDIT_BUTTON": "📈 Змінити відсоток",
+ "ADMIN_USER_REFERRAL_COMMISSION_PROMPT": "📈 Індивідуальний відсоток реферальної комісії\n\nПоточне значення: {current}%\nСтандартне значення: {default}%\n\nНадішліть нове значення від 0 до 100 або слово 'стандарт' для скидання.",
+ "ADMIN_USER_REFERRAL_COMMISSION_RESET_BUTTON": "♻️ Скинути на стандартний",
+ "ADMIN_USER_REFERRAL_COMMISSION_UPDATED": "✅ Відсоток оновлено: {percent}%",
+ "ADMIN_USER_REFERRAL_COMMISSION_INVALID": "❌ Введіть число від 0 до 100 або слово 'стандарт'",
+ "ADMIN_USER_REFERRALS_LIST_HEADER": "Список рефералів:",
+ "ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: {telegram_id}{username_part})",
+ "ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … і ще {count} рефералів",
"ADMIN_USER_REFERRALS_EMPTY": "Рефералів поки немає.",
"ADMIN_USER_REFERRALS_EDIT_HINT": "✏️ Щоб змінити список, натисніть «✏️ Редагувати» нижче.",
"ADMIN_USER_REFERRALS_EDIT_BUTTON": "✏️ Редагувати",
diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json
index 3c0b906f..d6c32b4e 100644
--- a/app/localization/locales/zh.json
+++ b/app/localization/locales/zh.json
@@ -733,6 +733,13 @@
"ADMIN_USER_REFERRALS_BUTTON":"🤝推荐",
"ADMIN_USER_REFERRALS_TITLE":"🤝用户推荐",
"ADMIN_USER_REFERRALS_SUMMARY":"👤{name}(ID:{telegram_id})\n👥总推荐数:{count}",
+"ADMIN_USER_REFERRAL_COMMISSION_DEFAULT":"• 佣金比例:{percent}%(默认值)",
+"ADMIN_USER_REFERRAL_COMMISSION_CUSTOM":"• 自定义比例:{percent}%(默认:{default_percent}%)",
+"ADMIN_USER_REFERRAL_COMMISSION_EDIT_BUTTON":"📈 修改比例",
+"ADMIN_USER_REFERRAL_COMMISSION_PROMPT":"📈 自定义推荐佣金比例\n\n当前值:{current}%\n默认值:{default}%\n\n发送0到100之间的数值或输入“标准”以恢复默认。",
+"ADMIN_USER_REFERRAL_COMMISSION_RESET_BUTTON":"♻️ 恢复默认",
+"ADMIN_USER_REFERRAL_COMMISSION_UPDATED":"✅ 比例已更新:{percent}%",
+"ADMIN_USER_REFERRAL_COMMISSION_INVALID":"❌ 请输入0到100之间的数字或“标准”",
"ADMIN_USER_REFERRALS_LIST_HEADER":"推荐列表:",
"ADMIN_USER_REFERRALS_LIST_ITEM":"•{name}(ID:{telegram_id}{username_part})",
"ADMIN_USER_REFERRALS_LIST_TRUNCATED":"•…以及其他{count}个推荐",
diff --git a/app/services/referral_service.py b/app/services/referral_service.py
index 2302cf45..5324a775 100644
--- a/app/services/referral_service.py
+++ b/app/services/referral_service.py
@@ -7,6 +7,7 @@ from app.config import settings
from app.database.crud.user import add_user_balance, get_user_by_id
from app.database.crud.referral import create_referral_earning
from app.database.models import TransactionType, ReferralEarning
+from app.utils.user_utils import get_effective_referral_commission_percent
logger = logging.getLogger(__name__)
@@ -48,8 +49,9 @@ async def process_referral_registration(
amount_kopeks=0,
reason="referral_registration_pending"
)
-
+
if bot:
+ commission_percent = get_effective_referral_commission_percent(referrer)
referral_notification = (
f"🎉 Добро пожаловать!\n\n"
f"Вы перешли по реферальной ссылке пользователя {referrer.full_name}!\n\n"
@@ -64,8 +66,8 @@ async def process_referral_registration(
f"По вашей ссылке зарегистрировался пользователь {new_user.full_name}!\n\n"
f"💰 Когда он пополнит баланс от {settings.format_price(settings.REFERRAL_MINIMUM_TOPUP_KOPEKS)}, "
f"вы получите минимум {settings.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS)} или "
- f"{settings.REFERRAL_COMMISSION_PERCENT}% от суммы (что больше).\n\n"
- f"📈 С каждого последующего пополнения вы будете получать {settings.REFERRAL_COMMISSION_PERCENT}% комиссии."
+ f"{commission_percent}% от суммы (что больше).\n\n"
+ f"📈 С каждого последующего пополнения вы будете получать {commission_percent}% комиссии."
)
await send_referral_notification(bot, referrer.telegram_id, inviter_notification)
@@ -94,13 +96,14 @@ async def process_referral_topup(
logger.error(f"Реферер {user.referred_by_id} не найден")
return False
+ commission_percent = get_effective_referral_commission_percent(referrer)
qualifies_for_first_bonus = (
topup_amount_kopeks >= settings.REFERRAL_MINIMUM_TOPUP_KOPEKS
)
commission_amount = 0
- if settings.REFERRAL_COMMISSION_PERCENT > 0:
+ if commission_percent > 0:
commission_amount = int(
- topup_amount_kopeks * settings.REFERRAL_COMMISSION_PERCENT / 100
+ topup_amount_kopeks * commission_percent / 100
)
if not user.has_made_first_topup:
@@ -116,7 +119,7 @@ async def process_referral_topup(
db,
referrer,
commission_amount,
- f"Комиссия {settings.REFERRAL_COMMISSION_PERCENT}% с пополнения {user.full_name}",
+ f"Комиссия {commission_percent}% с пополнения {user.full_name}",
bot=bot,
)
@@ -139,7 +142,7 @@ async def process_referral_topup(
f"💰 Реферальная комиссия!\n\n"
f"Ваш реферал {user.full_name} пополнил баланс на "
f"{settings.format_price(topup_amount_kopeks)}\n\n"
- f"🎁 Ваша комиссия ({settings.REFERRAL_COMMISSION_PERCENT}%): "
+ f"🎁 Ваша комиссия ({commission_percent}%): "
f"{settings.format_price(commission_amount)}\n\n"
f"💎 Средства зачислены на ваш баланс."
)
@@ -180,7 +183,7 @@ async def process_referral_topup(
)
await send_referral_notification(bot, user.telegram_id, bonus_notification)
- commission_amount = int(topup_amount_kopeks * settings.REFERRAL_COMMISSION_PERCENT / 100)
+ commission_amount = int(topup_amount_kopeks * commission_percent / 100)
inviter_bonus = max(settings.REFERRAL_INVITER_BONUS_KOPEKS, commission_amount)
if inviter_bonus > 0:
@@ -204,7 +207,7 @@ async def process_referral_topup(
f"💰 Реферальная награда!\n\n"
f"Ваш реферал {user.full_name} сделал первое пополнение!\n\n"
f"🎁 Вы получили награду: {settings.format_price(inviter_bonus)}\n\n"
- f"📈 Теперь с каждого его пополнения вы будете получать {settings.REFERRAL_COMMISSION_PERCENT}% комиссии."
+ f"📈 Теперь с каждого его пополнения вы будете получать {commission_percent}% комиссии."
)
await send_referral_notification(bot, referrer.telegram_id, inviter_bonus_notification)
@@ -212,7 +215,7 @@ async def process_referral_topup(
if commission_amount > 0:
await add_user_balance(
db, referrer, commission_amount,
- f"Комиссия {settings.REFERRAL_COMMISSION_PERCENT}% с пополнения {user.full_name}",
+ f"Комиссия {commission_percent}% с пополнения {user.full_name}",
bot=bot
)
@@ -231,7 +234,7 @@ async def process_referral_topup(
f"💰 Реферальная комиссия!\n\n"
f"Ваш реферал {user.full_name} пополнил баланс на "
f"{settings.format_price(topup_amount_kopeks)}\n\n"
- f"🎁 Ваша комиссия ({settings.REFERRAL_COMMISSION_PERCENT}%): "
+ f"🎁 Ваша комиссия ({commission_percent}%): "
f"{settings.format_price(commission_amount)}\n\n"
f"💎 Средства зачислены на ваш баланс."
)
@@ -261,11 +264,7 @@ async def process_referral_purchase(
logger.error(f"Реферер {user.referred_by_id} не найден")
return False
- if not (0 <= settings.REFERRAL_COMMISSION_PERCENT <= 100):
- logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: REFERRAL_COMMISSION_PERCENT = {settings.REFERRAL_COMMISSION_PERCENT} некорректный!")
- commission_percent = 10
- else:
- commission_percent = settings.REFERRAL_COMMISSION_PERCENT
+ commission_percent = get_effective_referral_commission_percent(referrer)
commission_amount = int(purchase_amount_kopeks * commission_percent / 100)
diff --git a/app/states.py b/app/states.py
index 83295d68..897297d3 100644
--- a/app/states.py
+++ b/app/states.py
@@ -96,6 +96,7 @@ class AdminStates(StatesGroup):
editing_user_devices = State()
editing_user_traffic = State()
editing_user_referrals = State()
+ editing_user_referral_percent = State()
editing_rules_page = State()
editing_privacy_policy = State()
diff --git a/app/utils/user_utils.py b/app/utils/user_utils.py
index 95f37de2..f34d0708 100644
--- a/app/utils/user_utils.py
+++ b/app/utils/user_utils.py
@@ -1,12 +1,14 @@
import logging
import secrets
import string
+import logging
from datetime import datetime, timedelta
from typing import Optional, Dict, List
from sqlalchemy import select, func, and_, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
+from app.config import settings
from app.database.models import User, ReferralEarning, Transaction, TransactionType
logger = logging.getLogger(__name__)
@@ -58,6 +60,25 @@ async def generate_unique_referral_code(db: AsyncSession, telegram_id: int) -> s
return f"ref{timestamp}"
+def get_effective_referral_commission_percent(user: User) -> int:
+ """Возвращает индивидуальный процент комиссии пользователя или дефолтное значение."""
+
+ percent = getattr(user, "referral_commission_percent", None)
+
+ if percent is None:
+ percent = settings.REFERRAL_COMMISSION_PERCENT
+
+ if percent < 0 or percent > 100:
+ logger.error(
+ "❌ Некорректный процент комиссии для пользователя %s: %s",
+ getattr(user, "telegram_id", None),
+ percent,
+ )
+ return max(0, min(100, settings.REFERRAL_COMMISSION_PERCENT))
+
+ return percent
+
+
async def mark_user_as_had_paid_subscription(db: AsyncSession, user: User) -> bool:
try:
if user.has_had_paid_subscription:
diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py
index 64f4c43c..85724dc5 100644
--- a/app/webapi/routes/miniapp.py
+++ b/app/webapi/routes/miniapp.py
@@ -96,6 +96,7 @@ from app.utils.telegram_webapp import (
parse_webapp_init_data,
)
from app.utils.user_utils import (
+ get_effective_referral_commission_percent,
get_detailed_referral_list,
get_user_referral_summary,
)
@@ -2447,7 +2448,12 @@ async def _build_referral_info(
minimum_topup_kopeks = int(referral_settings.get("minimum_topup_kopeks") or 0)
first_topup_bonus_kopeks = int(referral_settings.get("first_topup_bonus_kopeks") or 0)
inviter_bonus_kopeks = int(referral_settings.get("inviter_bonus_kopeks") or 0)
- commission_percent = float(referral_settings.get("commission_percent") or 0)
+ commission_percent = float(
+ get_effective_referral_commission_percent(user)
+ if user
+ else referral_settings.get("commission_percent")
+ or 0
+ )
terms = MiniAppReferralTerms(
minimum_topup_kopeks=minimum_topup_kopeks,
diff --git a/migrations/alembic/versions/e3c1e0b5b4a7_add_referral_commission_percent_to_users.py b/migrations/alembic/versions/e3c1e0b5b4a7_add_referral_commission_percent_to_users.py
new file mode 100644
index 00000000..a3708b2d
--- /dev/null
+++ b/migrations/alembic/versions/e3c1e0b5b4a7_add_referral_commission_percent_to_users.py
@@ -0,0 +1,19 @@
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = "e3c1e0b5b4a7"
+down_revision: Union[str, None] = "c2f9c3b5f5c4"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ op.add_column("users", sa.Column("referral_commission_percent", sa.Integer(), nullable=True))
+
+
+def downgrade() -> None:
+ op.drop_column("users", "referral_commission_percent")