mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Merge pull request #2002 from BEDOLAGA-DEV/j33on7-bedolaga/add-individual-referral-percentage-in-user-edit
Add universal migration for referral commission column
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,6 +12,8 @@ docker-compose.override.yml
|
||||
!requirements.txt
|
||||
!docs/
|
||||
!docs/**
|
||||
!migrations/
|
||||
!migrations/**
|
||||
|
||||
# Разрешаем папку app/ и все её содержимое рекурсивно
|
||||
!app/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
"🤝 <b>Рефералы пользователя</b>",
|
||||
@@ -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",
|
||||
(
|
||||
"📈 <b>Индивидуальный процент реферальной комиссии</b>\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_")
|
||||
|
||||
@@ -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",
|
||||
"• Комиссия с каждого пополнения реферала: <b>{percent}%</b>",
|
||||
).format(percent=settings.REFERRAL_COMMISSION_PERCENT)
|
||||
).format(percent=get_effective_referral_commission_percent(db_user))
|
||||
+ "\n\n"
|
||||
+ texts.t("REFERRAL_LINK_TITLE", "🔗 <b>Ваша реферальная ссылка:</b>")
|
||||
+ f"\n<code>{referral_link}</code>\n\n"
|
||||
|
||||
@@ -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)}",
|
||||
|
||||
@@ -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": "🤝 <b>User referrals</b>",
|
||||
"ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: <code>{telegram_id}</code>)\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": "📈 <b>Custom referral commission</b>\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": "<b>List of referrals:</b>",
|
||||
"ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: <code>{telegram_id}</code>{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}%",
|
||||
|
||||
@@ -734,6 +734,13 @@
|
||||
"ADMIN_USER_REFERRALS_BUTTON": "🤝 Рефералы",
|
||||
"ADMIN_USER_REFERRALS_TITLE": "🤝 <b>Рефералы пользователя</b>",
|
||||
"ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: <code>{telegram_id}</code>)\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": "📈 <b>Индивидуальный процент реферальной комиссии</b>\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": "<b>Список рефералов:</b>",
|
||||
"ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: <code>{telegram_id}</code>{username_part})",
|
||||
"ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … и ещё {count} рефералов",
|
||||
|
||||
@@ -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": "🤝 <b>Реферали користувача</b>",
|
||||
"ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: <code>{telegram_id}</code>)\n👥 Всього рефералів: {count}",
|
||||
"ADMIN_USER_REFERRALS_LIST_HEADER": "<b>Список рефералів:</b>",
|
||||
"ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: <code>{telegram_id}</code>{username_part})",
|
||||
"ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … і ще {count} рефералів",
|
||||
"ADMIN_USER_REFERRALS_BUTTON": "🤝 Реферали",
|
||||
"ADMIN_USER_REFERRALS_TITLE": "🤝 <b>Реферали користувача</b>",
|
||||
"ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: <code>{telegram_id}</code>)\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": "📈 <b>Індивідуальний відсоток реферальної комісії</b>\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": "<b>Список рефералів:</b>",
|
||||
"ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: <code>{telegram_id}</code>{username_part})",
|
||||
"ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … і ще {count} рефералів",
|
||||
"ADMIN_USER_REFERRALS_EMPTY": "Рефералів поки немає.",
|
||||
"ADMIN_USER_REFERRALS_EDIT_HINT": "✏️ Щоб змінити список, натисніть «✏️ Редагувати» нижче.",
|
||||
"ADMIN_USER_REFERRALS_EDIT_BUTTON": "✏️ Редагувати",
|
||||
|
||||
@@ -733,6 +733,13 @@
|
||||
"ADMIN_USER_REFERRALS_BUTTON":"🤝推荐",
|
||||
"ADMIN_USER_REFERRALS_TITLE":"🤝<b>用户推荐</b>",
|
||||
"ADMIN_USER_REFERRALS_SUMMARY":"👤{name}(ID:<code>{telegram_id}</code>)\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":"📈 <b>自定义推荐佣金比例</b>\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":"<b>推荐列表:</b>",
|
||||
"ADMIN_USER_REFERRALS_LIST_ITEM":"•{name}(ID:<code>{telegram_id}</code>{username_part})",
|
||||
"ADMIN_USER_REFERRALS_LIST_TRUNCATED":"•…以及其他{count}个推荐",
|
||||
|
||||
@@ -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"🎉 <b>Добро пожаловать!</b>\n\n"
|
||||
f"Вы перешли по реферальной ссылке пользователя <b>{referrer.full_name}</b>!\n\n"
|
||||
@@ -64,8 +66,8 @@ async def process_referral_registration(
|
||||
f"По вашей ссылке зарегистрировался пользователь <b>{new_user.full_name}</b>!\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"💰 <b>Реферальная комиссия!</b>\n\n"
|
||||
f"Ваш реферал <b>{user.full_name}</b> пополнил баланс на "
|
||||
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"💰 <b>Реферальная награда!</b>\n\n"
|
||||
f"Ваш реферал <b>{user.full_name}</b> сделал первое пополнение!\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"💰 <b>Реферальная комиссия!</b>\n\n"
|
||||
f"Ваш реферал <b>{user.full_name}</b> пополнил баланс на "
|
||||
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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user