diff --git a/app/config.py b/app/config.py
index 494d9449..14f6bffe 100644
--- a/app/config.py
+++ b/app/config.py
@@ -150,10 +150,6 @@ class Settings(BaseSettings):
REFERRAL_PROGRAM_ENABLED: bool = True
REFERRAL_NOTIFICATIONS_ENABLED: bool = True
REFERRAL_NOTIFICATION_RETRY_ATTEMPTS: int = 3
- REFERRAL_WITHDRAWALS_ENABLED: bool = False
- REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS: int = 50000
- REFERRAL_WITHDRAWAL_PROMPT_TEXT: str = ""
- REFERRAL_WITHDRAWAL_SUCCESS_TEXT: str = ""
AUTOPAY_WARNING_DAYS: str = "3,1"
diff --git a/app/database/crud/referral_withdrawal.py b/app/database/crud/referral_withdrawal.py
deleted file mode 100644
index 9b36eb3f..00000000
--- a/app/database/crud/referral_withdrawal.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import logging
-from datetime import datetime
-from typing import Iterable, Optional
-
-from sqlalchemy import func, select
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import selectinload
-
-from app.database.models import ReferralWithdrawalRequest
-
-logger = logging.getLogger(__name__)
-
-
-async def create_referral_withdrawal_request(
- db: AsyncSession,
- user_id: int,
- amount_kopeks: int,
- requisites: str,
- status: str = "pending",
-) -> ReferralWithdrawalRequest:
- request = ReferralWithdrawalRequest(
- user_id=user_id,
- amount_kopeks=amount_kopeks,
- requisites=requisites,
- status=status,
- )
-
- db.add(request)
- await db.commit()
- await db.refresh(request)
-
- logger.info(
- "💸 Создан запрос на вывод партнёрки: %s₽ для пользователя %s",
- amount_kopeks / 100,
- user_id,
- )
- return request
-
-
-async def get_total_requested_amount(
- db: AsyncSession,
- user_id: int,
- statuses: Optional[Iterable[str]] = None,
-) -> int:
- query = select(func.coalesce(func.sum(ReferralWithdrawalRequest.amount_kopeks), 0)).where(
- ReferralWithdrawalRequest.user_id == user_id
- )
-
- if statuses:
- query = query.where(ReferralWithdrawalRequest.status.in_(list(statuses)))
-
- result = await db.execute(query)
- return result.scalar() or 0
-
-
-async def get_referral_withdrawal_requests(
- db: AsyncSession,
- status: Optional[str] = None,
- limit: int = 50,
- offset: int = 0,
-):
- query = select(ReferralWithdrawalRequest).options(
- selectinload(ReferralWithdrawalRequest.user),
- selectinload(ReferralWithdrawalRequest.closed_by),
- ).order_by(ReferralWithdrawalRequest.created_at.desc())
-
- if status:
- query = query.where(ReferralWithdrawalRequest.status == status)
-
- result = await db.execute(query.offset(offset).limit(limit))
- return result.scalars().all()
-
-
-async def get_referral_withdrawal_request_by_id(
- db: AsyncSession, request_id: int
-) -> Optional[ReferralWithdrawalRequest]:
- result = await db.execute(
- select(ReferralWithdrawalRequest)
- .options(
- selectinload(ReferralWithdrawalRequest.user),
- selectinload(ReferralWithdrawalRequest.closed_by),
- )
- .where(ReferralWithdrawalRequest.id == request_id)
- )
- return result.scalar_one_or_none()
-
-
-async def close_referral_withdrawal_request(
- db: AsyncSession, request: ReferralWithdrawalRequest, closed_by_id: Optional[int]
-) -> ReferralWithdrawalRequest:
- request.status = "closed"
- request.closed_by_id = closed_by_id
- request.closed_at = datetime.utcnow()
-
- await db.commit()
- await db.refresh(request)
- return request
-
diff --git a/app/database/models.py b/app/database/models.py
index b2e6c32f..f8e9f719 100644
--- a/app/database/models.py
+++ b/app/database/models.py
@@ -957,29 +957,6 @@ class ReferralEarning(Base):
return self.amount_kopeks / 100
-class ReferralWithdrawalRequest(Base):
- __tablename__ = "referral_withdrawal_requests"
-
- id = Column(Integer, primary_key=True, index=True)
- user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
- amount_kopeks = Column(Integer, nullable=False)
- requisites = Column(Text, nullable=False)
- status = Column(String(32), nullable=False, default="pending")
-
- closed_by_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
- closed_at = Column(DateTime, nullable=True)
-
- created_at = Column(DateTime, default=func.now())
- updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
-
- user = relationship("User", foreign_keys=[user_id])
- closed_by = relationship("User", foreign_keys=[closed_by_id])
-
- @property
- def amount_rubles(self) -> float:
- return self.amount_kopeks / 100
-
-
class Squad(Base):
__tablename__ = "squads"
diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py
index 47ffbc9c..6bf30aff 100644
--- a/app/database/universal_migration.py
+++ b/app/database/universal_migration.py
@@ -3819,86 +3819,6 @@ async def add_promocode_promo_group_column() -> bool:
return False
-async def create_referral_withdrawal_requests_table() -> bool:
- table_exists = await check_table_exists("referral_withdrawal_requests")
- if table_exists:
- logger.info("ℹ️ Таблица referral_withdrawal_requests уже существует")
- return True
-
- try:
- async with engine.begin() as conn:
- db_type = await get_database_type()
-
- if db_type == "sqlite":
- create_table_sql = """
- CREATE TABLE referral_withdrawal_requests (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- amount_kopeks INTEGER NOT NULL,
- requisites TEXT NOT NULL,
- status VARCHAR(32) NOT NULL DEFAULT 'pending',
- closed_by_id INTEGER NULL,
- closed_at DATETIME NULL,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
- FOREIGN KEY (closed_by_id) REFERENCES users(id) ON DELETE SET NULL
- );
- """
- create_index_sql = (
- "CREATE INDEX idx_referral_withdrawals_user "
- "ON referral_withdrawal_requests(user_id);"
- )
- elif db_type == "postgresql":
- create_table_sql = """
- CREATE TABLE referral_withdrawal_requests (
- id SERIAL PRIMARY KEY,
- user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- amount_kopeks INTEGER NOT NULL,
- requisites TEXT NOT NULL,
- status VARCHAR(32) NOT NULL DEFAULT 'pending',
- closed_by_id INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
- closed_at TIMESTAMP NULL,
- created_at TIMESTAMP DEFAULT NOW(),
- updated_at TIMESTAMP DEFAULT NOW()
- );
- """
- create_index_sql = (
- "CREATE INDEX idx_referral_withdrawals_user "
- "ON referral_withdrawal_requests(user_id);"
- )
- else: # MySQL
- create_table_sql = """
- CREATE TABLE referral_withdrawal_requests (
- id INT AUTO_INCREMENT PRIMARY KEY,
- user_id INT NOT NULL,
- amount_kopeks INT NOT NULL,
- requisites TEXT NOT NULL,
- status VARCHAR(32) NOT NULL DEFAULT 'pending',
- closed_by_id INT NULL,
- closed_at TIMESTAMP NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- CONSTRAINT fk_ref_withdraw_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
- CONSTRAINT fk_ref_withdraw_closed_by FOREIGN KEY (closed_by_id) REFERENCES users(id) ON DELETE SET NULL
- );
- """
- create_index_sql = (
- "CREATE INDEX idx_referral_withdrawals_user "
- "ON referral_withdrawal_requests(user_id);"
- )
-
- await conn.execute(text(create_table_sql))
- await conn.execute(text(create_index_sql))
-
- logger.info("✅ Таблица referral_withdrawal_requests создана")
- return True
-
- except Exception as error:
- logger.error(f"❌ Ошибка создания таблицы referral_withdrawal_requests: {error}")
- return False
-
-
async def run_universal_migration():
logger.info("=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===")
@@ -4101,13 +4021,6 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с таблицей main_menu_buttons")
- logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ REFERRAL_WITHDRAWAL_REQUESTS ===")
- referral_withdrawals_created = await create_referral_withdrawal_requests_table()
- if referral_withdrawals_created:
- logger.info("✅ Таблица заявок на вывод партнёрки готова")
- else:
- logger.warning("⚠️ Проблемы с таблицей заявок на вывод партнёрки")
-
template_columns_ready = await ensure_promo_offer_template_active_duration_column()
if template_columns_ready:
logger.info("✅ Колонка active_discount_hours промо-предложений готова")
@@ -4354,7 +4267,6 @@ async def check_migration_status():
"promo_offer_templates_active_discount_column": False,
"promo_offer_logs_table": False,
"subscription_temporary_access_table": False,
- "referral_withdrawal_requests_table": False,
}
status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup')
@@ -4378,7 +4290,6 @@ async def check_migration_status():
status["promo_offer_templates_active_discount_column"] = await check_column_exists('promo_offer_templates', 'active_discount_hours')
status["promo_offer_logs_table"] = await check_table_exists('promo_offer_logs')
status["subscription_temporary_access_table"] = await check_table_exists('subscription_temporary_access')
- status["referral_withdrawal_requests_table"] = await check_table_exists('referral_withdrawal_requests')
status["welcome_texts_is_enabled_column"] = await check_column_exists('welcome_texts', 'is_enabled')
status["users_promo_group_column"] = await check_column_exists('users', 'promo_group_id')
@@ -4447,7 +4358,6 @@ async def check_migration_status():
"promo_offer_templates_active_discount_column": "Колонка active_discount_hours в promo_offer_templates",
"promo_offer_logs_table": "Таблица promo_offer_logs",
"subscription_temporary_access_table": "Таблица subscription_temporary_access",
- "referral_withdrawal_requests_table": "Таблица заявок на вывод партнёрки",
}
for check_key, check_status in status.items():
diff --git a/app/handlers/admin/referrals.py b/app/handlers/admin/referrals.py
index f6c49322..9de1bb65 100644
--- a/app/handlers/admin/referrals.py
+++ b/app/handlers/admin/referrals.py
@@ -1,17 +1,13 @@
import logging
-import datetime
-from html import escape
from aiogram import Dispatcher, types, F
-from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
+import datetime
from app.config import settings
from app.database.models import User
from app.localization.texts import get_texts
from app.database.crud.referral import get_referral_statistics, get_user_referral_stats
from app.database.crud.user import get_user_by_id
-from app.services.referral_withdrawal_service import ReferralWithdrawalService
-from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
logger = logging.getLogger(__name__)
@@ -25,7 +21,6 @@ async def show_referral_statistics(
db: AsyncSession
):
try:
- texts = get_texts(db_user.language)
stats = await get_referral_statistics(db)
avg_per_referrer = 0
@@ -82,12 +77,6 @@ async def show_referral_statistics(
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_referrals")],
[types.InlineKeyboardButton(text="👥 Топ рефереров", callback_data="admin_referrals_top")],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_REFERRAL_WITHDRAWALS", "💸 Вывод партнёрки"),
- callback_data="admin_referral_withdrawals",
- )
- ],
[types.InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_referrals_settings")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")]
])
@@ -213,428 +202,12 @@ async def show_referral_settings(
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⬅️ К статистике", callback_data="admin_referrals")]
])
-
+
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
-@admin_required
-@error_handler
-async def show_referral_withdrawals_settings(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- settings_obj = await ReferralWithdrawalService.get_settings(db)
- pending_requests = await ReferralWithdrawalService.list_requests(
- db, status="pending", limit=200
- )
-
- text = (
- texts.t("ADMIN_REFERRAL_WITHDRAWALS_TITLE", "💸 Вывод партнёрки")
- + "\n\n"
- + texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_STATUS",
- "Статус: {status}",
- ).format(status="✅ Включен" if settings_obj.enabled else "❌ Выключен")
- + "\n"
- + texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_AMOUNT",
- "Минимальная сумма: {amount}",
- ).format(amount=settings.format_price(settings_obj.min_amount_kopeks))
- + "\n"
- + texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_PENDING",
- "Заявок в ожидании: {count}",
- ).format(count=len(pending_requests))
- + "\n\n"
- + texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_TEXT",
- "Текст запроса:\n{prompt}",
- ).format(prompt=settings_obj.prompt_text)
- + "\n\n"
- + texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_TEXT",
- "Текст подтверждения:\n{success}",
- ).format(success=settings_obj.success_text)
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=("⏸️ Выключить" if settings_obj.enabled else "▶️ Включить"),
- callback_data="admin_referral_withdrawals_toggle",
- )
- ],
- [
- types.InlineKeyboardButton(
- text="✏️ Минимальная сумма",
- callback_data="admin_referral_withdrawals_min",
- )
- ],
- [
- types.InlineKeyboardButton(
- text="✏️ Текст запроса",
- callback_data="admin_referral_withdrawals_prompt",
- )
- ],
- [
- types.InlineKeyboardButton(
- text="✏️ Текст подтверждения",
- callback_data="admin_referral_withdrawals_success",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_REFERRAL_WITHDRAWALS", "💸 Вывод партнёрки"),
- callback_data="admin_referral_withdrawal_requests",
- )
- ],
- [types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referrals")],
- ]
- )
-
- await state.set_state(None)
- await callback.message.edit_text(text, reply_markup=keyboard)
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def toggle_referral_withdrawals(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- current_settings = await ReferralWithdrawalService.get_settings(db)
- await ReferralWithdrawalService.set_enabled(db, not current_settings.enabled)
- await callback.answer("Изменено")
- await show_referral_withdrawals_settings(callback, db_user, db, state)
-
-
-@admin_required
-@error_handler
-async def prompt_referral_withdraw_min_amount(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- await state.set_state(AdminStates.editing_referral_withdraw_min_amount)
- await callback.message.edit_text(
- texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_PROMPT",
- "Введите минимальную сумму для вывода (в рублях):",
- ),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]]
- ),
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def prompt_referral_withdraw_prompt_text(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- await state.set_state(AdminStates.editing_referral_withdraw_prompt_text)
- await callback.message.edit_text(
- texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_EDIT",
- "Отправьте текст, который увидит пользователь при запросе вывода."
- "\nДоступные плейсхолдеры: {available}, {min_amount}.",
- ),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]]
- ),
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def prompt_referral_withdraw_success_text(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- await state.set_state(AdminStates.editing_referral_withdraw_success_text)
- await callback.message.edit_text(
- texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_EDIT",
- "Отправьте текст подтверждения после создания заявки."
- "\nДоступные плейсхолдеры: {amount}, {available}.",
- ),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]]
- ),
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def handle_referral_withdraw_min_amount(
- message: types.Message,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- amount_kopeks = ReferralWithdrawalService.parse_amount_to_kopeks(message.text or "")
- if amount_kopeks is None:
- await message.answer(
- texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_INVALID",
- "Некорректная сумма. Введите число, например 500 или 750.50",
- )
- )
- return
-
- await ReferralWithdrawalService.set_min_amount(db, amount_kopeks)
- await state.set_state(None)
- await message.answer(
- texts.t("ADMIN_REFERRAL_WITHDRAWALS_MIN_SAVED", "Минимум сохранён."),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]]
- ),
- )
-
-
-@admin_required
-@error_handler
-async def handle_referral_withdraw_prompt_text(
- message: types.Message,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- template = message.text or ""
-
- if not ReferralWithdrawalService.validate_prompt_template(template):
- await message.answer(
- texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_INVALID_TEMPLATE",
- "Используйте только плейсхолдеры {available} и {min_amount}.",
- )
- )
- return
-
- await ReferralWithdrawalService.set_prompt_text(db, template)
- await state.set_state(None)
- await message.answer(
- texts.t("ADMIN_REFERRAL_WITHDRAWALS_PROMPT_SAVED", "Текст сохранён."),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]]
- ),
- )
-
-
-@admin_required
-@error_handler
-async def handle_referral_withdraw_success_text(
- message: types.Message,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- template = message.text or ""
-
- if not ReferralWithdrawalService.validate_success_template(template):
- await message.answer(
- texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_INVALID_SUCCESS_TEMPLATE",
- "Используйте только плейсхолдеры {amount} и {available}.",
- )
- )
- return
-
- await ReferralWithdrawalService.set_success_text(db, template)
- await state.set_state(None)
- await message.answer(
- texts.t("ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_SAVED", "Текст сохранён."),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]]
- ),
- )
-
-
-@admin_required
-@error_handler
-async def show_referral_withdrawal_requests(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- requests = await ReferralWithdrawalService.list_requests(db, limit=50)
-
- if not requests:
- await callback.message.edit_text(
- texts.t("ADMIN_REFERRAL_WITHDRAWALS_EMPTY", "Нет заявок на вывод."),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]]
- ),
- )
- await callback.answer()
- return
-
- keyboard = []
- for request in requests:
- user_display = request.user.full_name if request.user else f"ID{request.user_id}"
- button_text = f"#{request.id} | {settings.format_price(request.amount_kopeks)} | {user_display}"
- keyboard.append(
- [
- types.InlineKeyboardButton(
- text=button_text,
- callback_data=f"admin_referral_withdrawal_{request.id}",
- )
- ]
- )
-
- keyboard.append(
- [types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawals")]
- )
-
- await callback.message.edit_text(
- texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_LIST_TITLE",
- "📨 Заявки на вывод партнёрки",
- ),
- reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def show_referral_withdrawal_request(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- request_id = int(callback.data.split("_")[-1])
- request = await ReferralWithdrawalService.get_request(db, request_id)
-
- if not request:
- await callback.answer("Заявка не найдена", show_alert=True)
- return
-
- user = request.user
- user_info = (
- f"{user.full_name}\n@{user.username}" if user and user.username else (user.full_name if user else "—")
- )
- escaped_requisites = escape(request.requisites or "—")
- text = (
- f"💸 Заявка #{request.id}\n\n"
- f"👤 {user_info}\n"
- f"🆔 {user.telegram_id if user else request.user_id}\n"
- f"💰 Сумма: {settings.format_price(request.amount_kopeks)}\n"
- f"📅 Создана: {request.created_at.strftime('%d.%m.%Y %H:%M')}\n"
- f"📌 Статус: {request.status}\n\n"
- f"🧾 Реквизиты:\n{escaped_requisites}"
- )
-
- if request.status == "closed" and request.closed_at:
- text += f"\n\n✅ Закрыта: {request.closed_at.strftime('%d.%m.%Y %H:%M')}"
- if request.closed_by:
- text += f"\n👮♂️ Закрыл: {request.closed_by.full_name}"
-
- keyboard = []
- if request.status != "closed":
- keyboard.append(
- [
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_REFERRAL_WITHDRAWALS_CLOSE", "✅ Закрыть заявку"
- ),
- callback_data=f"admin_referral_withdrawal_close_{request.id}",
- )
- ]
- )
- keyboard.append(
- [types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_referral_withdrawal_requests")]
- )
-
- await callback.message.edit_text(
- text,
- reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def close_referral_withdrawal_request(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- request_id = int(callback.data.split("_")[-1])
- closed = await ReferralWithdrawalService.close_request(db, request_id, db_user.id)
- if not closed:
- await callback.answer("Не удалось закрыть заявку", show_alert=True)
- return
- await callback.answer("Заявка закрыта")
- await show_referral_withdrawal_request(callback, db_user, db)
-
-
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_referral_statistics, F.data == "admin_referrals")
dp.callback_query.register(show_top_referrers, F.data == "admin_referrals_top")
dp.callback_query.register(show_referral_settings, F.data == "admin_referrals_settings")
- dp.callback_query.register(
- show_referral_withdrawals_settings,
- F.data == "admin_referral_withdrawals",
- )
- dp.callback_query.register(
- toggle_referral_withdrawals, F.data == "admin_referral_withdrawals_toggle"
- )
- dp.callback_query.register(
- prompt_referral_withdraw_min_amount,
- F.data == "admin_referral_withdrawals_min",
- )
- dp.callback_query.register(
- prompt_referral_withdraw_prompt_text,
- F.data == "admin_referral_withdrawals_prompt",
- )
- dp.callback_query.register(
- prompt_referral_withdraw_success_text,
- F.data == "admin_referral_withdrawals_success",
- )
- dp.callback_query.register(
- show_referral_withdrawal_requests,
- F.data == "admin_referral_withdrawal_requests",
- )
- dp.callback_query.register(
- close_referral_withdrawal_request,
- F.data.startswith("admin_referral_withdrawal_close_"),
- )
- dp.callback_query.register(
- show_referral_withdrawal_request,
- F.data.startswith("admin_referral_withdrawal_"),
- )
- dp.message.register(
- handle_referral_withdraw_min_amount,
- AdminStates.editing_referral_withdraw_min_amount,
- )
- dp.message.register(
- handle_referral_withdraw_prompt_text,
- AdminStates.editing_referral_withdraw_prompt_text,
- )
- dp.message.register(
- handle_referral_withdraw_success_text,
- AdminStates.editing_referral_withdraw_success_text,
- )
diff --git a/app/handlers/referral.py b/app/handlers/referral.py
index b77e737b..e2351719 100644
--- a/app/handlers/referral.py
+++ b/app/handlers/referral.py
@@ -3,7 +3,6 @@ from pathlib import Path
import qrcode
from aiogram import Dispatcher, F, types
-from aiogram.fsm.context import FSMContext
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import FSInputFile
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,8 +11,6 @@ from app.config import settings
from app.database.models import User
from app.keyboards.inline import get_referral_keyboard
from app.localization.texts import get_texts
-from app.services.referral_withdrawal_service import ReferralWithdrawalService
-from app.states import ReferralWithdrawalStates
from app.utils.photo_message import edit_or_answer_photo
from app.utils.user_utils import (
get_detailed_referral_list,
@@ -28,15 +25,11 @@ logger = logging.getLogger(__name__)
async def show_referral_info(
callback: types.CallbackQuery,
db_user: User,
- db: AsyncSession,
- state: FSMContext,
+ db: AsyncSession
):
- await state.clear()
-
texts = get_texts(db_user.language)
-
+
summary = await get_user_referral_summary(db, db_user.id)
- withdrawal_settings = await ReferralWithdrawalService.get_settings(db)
bot_username = (await callback.bot.get_me()).username
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
@@ -188,10 +181,7 @@ async def show_referral_info(
await edit_or_answer_photo(
callback,
referral_text,
- get_referral_keyboard(
- db_user.language,
- show_withdrawal_button=withdrawal_settings.enabled,
- ),
+ get_referral_keyboard(db_user.language),
)
await callback.answer()
@@ -464,124 +454,6 @@ async def create_invite_message(
await callback.answer()
-async def start_referral_withdrawal_request(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- settings_obj = await ReferralWithdrawalService.get_settings(db)
-
- if not settings_obj.enabled:
- await callback.answer(
- texts.t(
- "REFERRAL_WITHDRAWAL_DISABLED",
- "Вывод реферального дохода сейчас недоступен.",
- ),
- show_alert=True,
- )
- return
-
- available = await ReferralWithdrawalService.get_available_amount(db, db_user.id)
- if available < settings_obj.min_amount_kopeks:
- await callback.answer(
- texts.t(
- "REFERRAL_WITHDRAWAL_TOO_LOW",
- "Минимальная сумма для вывода: {min_amount}. Доступно: {available}.",
- ).format(
- min_amount=texts.format_price(settings_obj.min_amount_kopeks),
- available=texts.format_price(available),
- ),
- show_alert=True,
- )
- return
-
- fallback_prompt = texts.t(
- "REFERRAL_WITHDRAWAL_PROMPT",
- ReferralWithdrawalService.DEFAULT_PROMPT,
- )
- prompt_template = settings_obj.prompt_text or fallback_prompt
-
- prompt_text = ReferralWithdrawalService.format_prompt_text(
- prompt_template,
- {
- "available": texts.format_price(available),
- "min_amount": texts.format_price(settings_obj.min_amount_kopeks),
- },
- fallback_prompt,
- )
-
- await state.set_state(ReferralWithdrawalStates.waiting_for_requisites)
- await state.update_data(referral_withdraw_available=available)
- await callback.message.answer(
- prompt_text,
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_referrals")]]
- ),
- )
- await callback.answer()
-
-
-async def handle_referral_withdrawal_requisites(
- message: types.Message,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- requisites = (message.text or "").strip()
-
- if not requisites:
- await message.answer(
- texts.t(
- "REFERRAL_WITHDRAWAL_ENTER_REQUISITES",
- "Пожалуйста, отправьте реквизиты для вывода.",
- )
- )
- return
-
- request = await ReferralWithdrawalService.create_request(
- db, db_user.id, requisites
- )
- await state.clear()
-
- if not request:
- settings_obj = await ReferralWithdrawalService.get_settings(db)
- await message.answer(
- texts.t(
- "REFERRAL_WITHDRAWAL_TOO_LOW",
- "Минимальная сумма для вывода: {min_amount}. Доступно: {available}.",
- ).format(
- min_amount=texts.format_price(settings_obj.min_amount_kopeks),
- available=texts.format_price(
- await ReferralWithdrawalService.get_available_amount(db, db_user.id)
- ),
- )
- )
- return
-
- settings_obj = await ReferralWithdrawalService.get_settings(db)
- fallback_success = texts.t(
- "REFERRAL_WITHDRAWAL_SUBMITTED",
- ReferralWithdrawalService.DEFAULT_SUCCESS,
- )
- success_template = settings_obj.success_text or fallback_success
-
- await message.answer(
- ReferralWithdrawalService.format_success_text(
- success_template,
- {
- "amount": texts.format_price(request.amount_kopeks),
- "available": texts.format_price(
- await ReferralWithdrawalService.get_available_amount(db, db_user.id)
- ),
- },
- fallback_success,
- )
- )
-
-
def register_handlers(dp: Dispatcher):
dp.callback_query.register(
@@ -608,16 +480,6 @@ def register_handlers(dp: Dispatcher):
show_referral_analytics,
F.data == "referral_analytics"
)
-
- dp.callback_query.register(
- start_referral_withdrawal_request,
- F.data == "referral_withdrawal_request",
- )
-
- dp.message.register(
- handle_referral_withdrawal_requisites,
- ReferralWithdrawalStates.waiting_for_requisites,
- )
dp.callback_query.register(
lambda callback, db_user, db: show_detailed_referral_list(
diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py
index af800345..8cb6208d 100644
--- a/app/keyboards/inline.py
+++ b/app/keyboards/inline.py
@@ -1329,11 +1329,9 @@ def get_subscription_expiring_keyboard(subscription_id: int, language: str = DEF
]
])
-def get_referral_keyboard(
- language: str = DEFAULT_LANGUAGE, *, show_withdrawal_button: bool = False
-) -> InlineKeyboardMarkup:
+def get_referral_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
-
+
keyboard = [
[
InlineKeyboardButton(
@@ -1353,19 +1351,6 @@ def get_referral_keyboard(
callback_data="referral_list"
)
],
- ]
-
- if show_withdrawal_button:
- keyboard.append([
- InlineKeyboardButton(
- text=texts.t(
- "REFERRAL_WITHDRAWAL_BUTTON", "💸 Запросить вывод"
- ),
- callback_data="referral_withdrawal_request",
- )
- ])
-
- keyboard.extend([
[
InlineKeyboardButton(
text=texts.t("REFERRAL_ANALYTICS_BUTTON", "📊 Аналитика"),
@@ -1375,10 +1360,10 @@ def get_referral_keyboard(
[
InlineKeyboardButton(
text=texts.BACK,
- callback_data="back_to_menu"
+ callback_data="back_to_menu"
)
]
- ])
+ ]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index 778c3d9a..34067554 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -621,25 +621,6 @@
"ADMIN_STATISTICS": "📊 Statistics",
"ADMIN_STATS_BUTTON": "📊 Statistics",
"ADMIN_STATS_REFERRALS": "🤝 Referrals",
- "ADMIN_REFERRAL_WITHDRAWALS": "💸 Referral payouts",
- "ADMIN_REFERRAL_WITHDRAWALS_TITLE": "💸 Referral payouts",
- "ADMIN_REFERRAL_WITHDRAWALS_STATUS": "Status: {status}",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_AMOUNT": "Minimum amount: {amount}",
- "ADMIN_REFERRAL_WITHDRAWALS_PENDING": "Pending requests: {count}",
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_TEXT": "Request text:\n{prompt}",
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_TEXT": "Confirmation text:\n{success}",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_PROMPT": "Enter the minimum withdrawal amount:",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_INVALID": "Invalid amount. Send a number like 500 or 750.50",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_SAVED": "Minimum saved.",
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_SAVED": "Text saved.",
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_SAVED": "Text saved.",
- "ADMIN_REFERRAL_WITHDRAWALS_INVALID_TEMPLATE": "Only placeholders {available} and {min_amount} are allowed.",
- "ADMIN_REFERRAL_WITHDRAWALS_INVALID_SUCCESS_TEMPLATE": "Only placeholders {amount} and {available} are allowed.",
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_EDIT": "Send the text users will see when requesting a payout.\nAvailable placeholders: {available}, {min_amount}.",
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_EDIT": "Send the confirmation text after a request is created.\nAvailable placeholders: {amount}, {available}.",
- "ADMIN_REFERRAL_WITHDRAWALS_EMPTY": "No payout requests yet.",
- "ADMIN_REFERRAL_WITHDRAWALS_LIST_TITLE": "📨 Referral payout requests",
- "ADMIN_REFERRAL_WITHDRAWALS_CLOSE": "✅ Close request",
"ADMIN_STATS_REVENUE": "💰 Revenue",
"ADMIN_STATS_SUBSCRIPTIONS": "📱 Subscriptions",
"ADMIN_STATS_SUMMARY": "📊 Summary",
@@ -1210,12 +1191,6 @@
"REFERRAL_LINK_CAPTION": "🔗 Your referral link:\n{link}",
"REFERRAL_LINK_TITLE": "🔗 Your referral link:",
"REFERRAL_LIST_BUTTON": "👥 Referral list",
- "REFERRAL_WITHDRAWAL_BUTTON": "💸 Request payout",
- "REFERRAL_WITHDRAWAL_DISABLED": "Referral payouts are currently unavailable.",
- "REFERRAL_WITHDRAWAL_TOO_LOW": "Minimum payout: {min_amount}. Available: {available}.",
- "REFERRAL_WITHDRAWAL_ENTER_REQUISITES": "Please send payout details.",
- "REFERRAL_WITHDRAWAL_SUBMITTED": "✅ Payout request sent! We'll contact you for confirmation. Amount: {amount}.",
- "REFERRAL_WITHDRAWAL_PROMPT": "✉️ Share payout details and your contact method.\n\nAvailable to withdraw: {available}. Minimum: {min_amount}.",
"REFERRAL_LIST_EMPTY": "📋 You have no referrals yet.\n\nShare your referral link to start earning!",
"REFERRAL_LIST_HEADER": "👥 Your referrals (page {current}/{total})",
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Activity: {days} days ago",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index c4af7eb2..3519200a 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -621,25 +621,6 @@
"ADMIN_STATISTICS": "📊 Статистика",
"ADMIN_STATS_BUTTON": "📊 Статистика",
"ADMIN_STATS_REFERRALS": "🤝 Партнерка",
- "ADMIN_REFERRAL_WITHDRAWALS": "💸 Вывод партнёрки",
- "ADMIN_REFERRAL_WITHDRAWALS_TITLE": "💸 Вывод партнёрки",
- "ADMIN_REFERRAL_WITHDRAWALS_STATUS": "Статус: {status}",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_AMOUNT": "Минимальная сумма: {amount}",
- "ADMIN_REFERRAL_WITHDRAWALS_PENDING": "Заявок в ожидании: {count}",
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_TEXT": "Текст запроса:\n{prompt}",
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_TEXT": "Текст подтверждения:\n{success}",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_PROMPT": "Введите минимальную сумму для вывода (в рублях):",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_INVALID": "Некорректная сумма. Введите число, например 500 или 750.50",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_SAVED": "Минимум сохранён.",
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_SAVED": "Текст сохранён.",
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_SAVED": "Текст сохранён.",
- "ADMIN_REFERRAL_WITHDRAWALS_INVALID_TEMPLATE": "Используйте только плейсхолдеры {available} и {min_amount}.",
- "ADMIN_REFERRAL_WITHDRAWALS_INVALID_SUCCESS_TEMPLATE": "Используйте только плейсхолдеры {amount} и {available}.",
- "ADMIN_REFERRAL_WITHDRAWALS_PROMPT_EDIT": "Отправьте текст, который увидит пользователь при запросе вывода.\nДоступные плейсхолдеры: {available}, {min_amount}.",
- "ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_EDIT": "Отправьте текст подтверждения после создания заявки.\nДоступные плейсхолдеры: {amount}, {available}.",
- "ADMIN_REFERRAL_WITHDRAWALS_EMPTY": "Нет заявок на вывод.",
- "ADMIN_REFERRAL_WITHDRAWALS_LIST_TITLE": "📨 Заявки на вывод партнёрки",
- "ADMIN_REFERRAL_WITHDRAWALS_CLOSE": "✅ Закрыть заявку",
"ADMIN_STATS_REVENUE": "💰 Доходы",
"ADMIN_STATS_SUBSCRIPTIONS": "📱 Подписки",
"ADMIN_STATS_SUMMARY": "📊 Общая сводка",
@@ -1221,12 +1202,6 @@
"REFERRAL_LINK_CAPTION": "🔗 Ваша реферальная ссылка:\n{link}",
"REFERRAL_LINK_TITLE": "🔗 Ваша реферальная ссылка:",
"REFERRAL_LIST_BUTTON": "👥 Список рефералов",
- "REFERRAL_WITHDRAWAL_BUTTON": "💸 Запросить вывод",
- "REFERRAL_WITHDRAWAL_DISABLED": "Вывод реферального дохода сейчас недоступен.",
- "REFERRAL_WITHDRAWAL_TOO_LOW": "Минимальная сумма для вывода: {min_amount}. Доступно: {available}.",
- "REFERRAL_WITHDRAWAL_ENTER_REQUISITES": "Пожалуйста, отправьте реквизиты для вывода.",
- "REFERRAL_WITHDRAWAL_SUBMITTED": "✅ Заявка отправлена! Мы свяжемся с вами для подтверждения. Сумма: {amount}.",
- "REFERRAL_WITHDRAWAL_PROMPT": "✉️ Укажите реквизиты для вывода и удобный способ связи.\n\nДоступно к выводу: {available}. Минимум: {min_amount}.",
"REFERRAL_LIST_EMPTY": "📋 У вас пока нет рефералов.\n\nПоделитесь своей реферальной ссылкой, чтобы начать зарабатывать!",
"REFERRAL_LIST_HEADER": "👥 Ваши рефералы (стр. {current}/{total})",
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Активность: {days} дн. назад",
diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json
index 530c9b22..f0b24625 100644
--- a/app/localization/locales/ua.json
+++ b/app/localization/locales/ua.json
@@ -617,29 +617,10 @@
"ADMIN_SQUAD_MIGRATION_TITLE": "🚚 Переїзд сквадів",
"ADMIN_SQUAD_REMOVE_ALL": "❌ Видалити всіх користувачів",
"ADMIN_SQUAD_RENAME": "✏️ Перейменувати",
-"ADMIN_STATISTICS": "📊 Статистика",
-"ADMIN_STATS_BUTTON": "📊 Статистика",
-"ADMIN_STATS_REFERRALS": "🤝 Партнерка",
- "ADMIN_REFERRAL_WITHDRAWALS": "💸 Виведення партнерки",
- "ADMIN_REFERRAL_WITHDRAWALS_TITLE": "💸 Виведення партнерки",
- "ADMIN_REFERRAL_WITHDRAWALS_STATUS": "Статус: {status}",
- "ADMIN_REFERRAL_WITHDRAWALS_MIN_AMOUNT": "Мінімальна сума: {amount}",
- "ADMIN_REFERRAL_WITHDRAWALS_PENDING": "Заявок в очікуванні: {count}",
-"ADMIN_REFERRAL_WITHDRAWALS_PROMPT_TEXT": "Текст запиту:\n{prompt}",
-"ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_TEXT": "Текст підтвердження:\n{success}",
-"ADMIN_REFERRAL_WITHDRAWALS_MIN_PROMPT": "Введіть мінімальну суму для виведення (в гривнях):",
-"ADMIN_REFERRAL_WITHDRAWALS_MIN_INVALID": "Некоректна сума. Введіть число, наприклад 500 або 750.50",
-"ADMIN_REFERRAL_WITHDRAWALS_MIN_SAVED": "Мінімум збережено.",
-"ADMIN_REFERRAL_WITHDRAWALS_PROMPT_SAVED": "Текст збережено.",
-"ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_SAVED": "Текст збережено.",
-"ADMIN_REFERRAL_WITHDRAWALS_INVALID_TEMPLATE": "Використовуйте лише плейсхолдери {available} та {min_amount}.",
-"ADMIN_REFERRAL_WITHDRAWALS_INVALID_SUCCESS_TEMPLATE": "Використовуйте лише плейсхолдери {amount} та {available}.",
-"ADMIN_REFERRAL_WITHDRAWALS_PROMPT_EDIT": "Надішліть текст, який побачить користувач під час запиту виведення.\nДоступні плейсхолдери: {available}, {min_amount}.",
-"ADMIN_REFERRAL_WITHDRAWALS_SUCCESS_EDIT": "Надішліть текст підтвердження після створення заявки.\nДоступні плейсхолдери: {amount}, {available}.",
-"ADMIN_REFERRAL_WITHDRAWALS_EMPTY": "Немає заявок на виведення.",
- "ADMIN_REFERRAL_WITHDRAWALS_LIST_TITLE": "📨 Заявки на виведення партнерки",
- "ADMIN_REFERRAL_WITHDRAWALS_CLOSE": "✅ Закрити заявку",
-"ADMIN_STATS_REVENUE": "💰 Доходи",
+ "ADMIN_STATISTICS": "📊 Статистика",
+ "ADMIN_STATS_BUTTON": "📊 Статистика",
+ "ADMIN_STATS_REFERRALS": "🤝 Партнерка",
+ "ADMIN_STATS_REVENUE": "💰 Доходи",
"ADMIN_STATS_SUBSCRIPTIONS": "📱 Підписки",
"ADMIN_STATS_SUMMARY": "📊 Загальне зведення",
"ADMIN_STATS_USERS": "👥 Користувачі",
@@ -1216,17 +1197,11 @@
"REFERRAL_INVITE_LINK_PROMPT": "👇 Переходь за посиланням:",
"REFERRAL_INVITE_MESSAGE": "\n🎯 Запрошення до VPN сервісу\n\nПривіт! Запрошую тебе у відмінний VPN сервіс!\n\n🎁 За моїм посиланням ти отримаєш бонус: {bonus}\n\n🔗 Переходь: {link}\n🎫 Або використовуй промокод: {code}\n\n💪 Швидко, надійно, недорого!\n",
"REFERRAL_INVITE_TITLE": "🎉 Приєднуйся до VPN сервісу!",
-"REFERRAL_LINK_CAPTION": "🔗 Ваше реферальне посилання:\n{link}",
-"REFERRAL_LINK_TITLE": "🔗 Ваше реферальне посилання:",
-"REFERRAL_LIST_BUTTON": "👥 Список рефералів",
-"REFERRAL_WITHDRAWAL_BUTTON": "💸 Запит на виведення",
-"REFERRAL_WITHDRAWAL_DISABLED": "Виведення реферального доходу зараз недоступне.",
-"REFERRAL_WITHDRAWAL_TOO_LOW": "Мінімальна сума для виведення: {min_amount}. Доступно: {available}.",
-"REFERRAL_WITHDRAWAL_ENTER_REQUISITES": "Будь ласка, надішліть реквізити для виведення.",
-"REFERRAL_WITHDRAWAL_SUBMITTED": "✅ Заявку на виведення відправлено! Ми зв'яжемося з вами для підтвердження. Сума: {amount}.",
-"REFERRAL_WITHDRAWAL_PROMPT": "✉️ Вкажіть реквізити для виведення та зручний спосіб зв'язку.\n\nДоступно до виведення: {available}. Мінімум: {min_amount}.",
-"REFERRAL_LIST_EMPTY": "📋 У вас поки немає рефералів.\n\nПоділіться своїм реферальним посиланням, щоб почати заробляти!",
-"REFERRAL_LIST_HEADER": "👥 Ваші реферали (стор. {current}/{total})",
+ "REFERRAL_LINK_CAPTION": "🔗 Ваше реферальне посилання:\n{link}",
+ "REFERRAL_LINK_TITLE": "🔗 Ваше реферальне посилання:",
+ "REFERRAL_LIST_BUTTON": "👥 Список рефералів",
+ "REFERRAL_LIST_EMPTY": "📋 У вас поки немає рефералів.\n\nПоділіться своїм реферальним посиланням, щоб почати заробляти!",
+ "REFERRAL_LIST_HEADER": "👥 Ваші реферали (стор. {current}/{total})",
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Активність: {days} дн. тому",
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO": " 🕐 Активність: давно",
"REFERRAL_LIST_ITEM_EARNED": " 💎 Зароблено з нього: {amount}",
diff --git a/app/services/referral_withdrawal_service.py b/app/services/referral_withdrawal_service.py
deleted file mode 100644
index b6e36f84..00000000
--- a/app/services/referral_withdrawal_service.py
+++ /dev/null
@@ -1,278 +0,0 @@
-from __future__ import annotations
-
-import logging
-import string
-from dataclasses import dataclass
-from typing import Optional
-
-from sqlalchemy import select
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.config import settings
-from app.database.crud.referral_withdrawal import (
- close_referral_withdrawal_request,
- create_referral_withdrawal_request,
- get_referral_withdrawal_request_by_id,
- get_referral_withdrawal_requests,
- get_total_requested_amount,
-)
-from app.database.crud.system_setting import upsert_system_setting
-from app.database.crud.user import subtract_user_balance
-from app.database.models import ReferralWithdrawalRequest, SystemSetting
-from app.utils.user_utils import get_user_referral_summary
-
-logger = logging.getLogger(__name__)
-
-
-@dataclass(slots=True)
-class ReferralWithdrawalSettings:
- enabled: bool
- min_amount_kopeks: int
- prompt_text: str
- success_text: str
-
-
-class ReferralWithdrawalService:
- ENABLED_KEY = "REFERRAL_WITHDRAWALS_ENABLED"
- MIN_AMOUNT_KEY = "REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS"
- PROMPT_TEXT_KEY = "REFERRAL_WITHDRAWAL_PROMPT_TEXT"
- SUCCESS_TEXT_KEY = "REFERRAL_WITHDRAWAL_SUCCESS_TEXT"
-
- PROMPT_ALLOWED_FIELDS = {"available", "min_amount"}
- SUCCESS_ALLOWED_FIELDS = {"amount", "available"}
-
- DEFAULT_PROMPT = (
- "✉️ Укажите реквизиты для вывода и удобный способ связи."
- "\n\nДоступно к выводу: {available}. Минимум: {min_amount}."
- )
- DEFAULT_SUCCESS = (
- "✅ Заявка отправлена! Мы свяжемся с вами, когда обработаем выплату."
- )
-
- @classmethod
- async def get_settings(cls, db: AsyncSession) -> ReferralWithdrawalSettings:
- result = await db.execute(
- select(SystemSetting.key, SystemSetting.value).where(
- SystemSetting.key.in_(
- [
- cls.ENABLED_KEY,
- cls.MIN_AMOUNT_KEY,
- cls.PROMPT_TEXT_KEY,
- cls.SUCCESS_TEXT_KEY,
- ]
- )
- )
- )
- rows = dict(result.all())
-
- enabled = cls._parse_bool(
- rows.get(cls.ENABLED_KEY), settings.REFERRAL_WITHDRAWALS_ENABLED
- )
- min_amount = cls._parse_int(
- rows.get(cls.MIN_AMOUNT_KEY), settings.REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS
- )
- prompt_text = rows.get(cls.PROMPT_TEXT_KEY) or settings.REFERRAL_WITHDRAWAL_PROMPT_TEXT
- success_text = rows.get(cls.SUCCESS_TEXT_KEY) or settings.REFERRAL_WITHDRAWAL_SUCCESS_TEXT
-
- return ReferralWithdrawalSettings(
- enabled=enabled,
- min_amount_kopeks=max(min_amount, 0),
- prompt_text=prompt_text or cls.DEFAULT_PROMPT,
- success_text=success_text or cls.DEFAULT_SUCCESS,
- )
-
- @classmethod
- async def set_enabled(cls, db: AsyncSession, enabled: bool) -> None:
- await upsert_system_setting(db, cls.ENABLED_KEY, "1" if enabled else "0")
- await db.commit()
-
- @classmethod
- async def set_min_amount(cls, db: AsyncSession, amount_kopeks: int) -> None:
- amount = max(int(amount_kopeks), 0)
- await upsert_system_setting(db, cls.MIN_AMOUNT_KEY, str(amount))
- await db.commit()
-
- @classmethod
- async def set_prompt_text(cls, db: AsyncSession, text: str) -> None:
- await upsert_system_setting(db, cls.PROMPT_TEXT_KEY, text)
- await db.commit()
-
- @classmethod
- async def set_success_text(cls, db: AsyncSession, text: str) -> None:
- await upsert_system_setting(db, cls.SUCCESS_TEXT_KEY, text)
- await db.commit()
-
- @classmethod
- def _validate_template_fields(cls, template: str, allowed_fields: set[str]) -> bool:
- formatter = string.Formatter()
- try:
- for _literal, field_name, _format_spec, _conversion in formatter.parse(
- template
- ):
- if field_name and field_name not in allowed_fields:
- return False
-
- template.format(**{field: "" for field in allowed_fields})
- except (KeyError, ValueError):
- return False
-
- return True
-
- @classmethod
- def validate_prompt_template(cls, template: str) -> bool:
- return cls._validate_template_fields(template, cls.PROMPT_ALLOWED_FIELDS)
-
- @classmethod
- def validate_success_template(cls, template: str) -> bool:
- return cls._validate_template_fields(template, cls.SUCCESS_ALLOWED_FIELDS)
-
- @classmethod
- def _safe_format_template(
- cls,
- template: str,
- values: dict[str, str],
- allowed_fields: set[str],
- fallback_template: str,
- ) -> str:
- if not cls._validate_template_fields(template, allowed_fields):
- logger.warning("Неверные плейсхолдеры в шаблоне вывода: %s", template)
- template = fallback_template
-
- try:
- return template.format(**values)
- except Exception:
- logger.exception("Ошибка форматирования шаблона вывода: %s", template)
- return fallback_template.format(**values)
-
- @classmethod
- def format_prompt_text(
- cls,
- template: str,
- values: dict[str, str],
- fallback_template: str,
- ) -> str:
- return cls._safe_format_template(
- template, values, cls.PROMPT_ALLOWED_FIELDS, fallback_template
- )
-
- @classmethod
- def format_success_text(
- cls,
- template: str,
- values: dict[str, str],
- fallback_template: str,
- ) -> str:
- return cls._safe_format_template(
- template, values, cls.SUCCESS_ALLOWED_FIELDS, fallback_template
- )
-
- @classmethod
- async def get_available_amount(cls, db: AsyncSession, user_id: int) -> int:
- summary = await get_user_referral_summary(db, user_id)
- total_earned = summary.get("total_earned_kopeks", 0)
- already_requested = await get_total_requested_amount(db, user_id)
- available = max(total_earned - already_requested, 0)
- logger.debug(
- "Расчёт доступного реферального дохода: total=%s, requested=%s, available=%s",
- total_earned,
- already_requested,
- available,
- )
- return available
-
- @classmethod
- async def create_request(
- cls, db: AsyncSession, user_id: int, requisites: str
- ) -> Optional[ReferralWithdrawalRequest]:
- settings_obj = await cls.get_settings(db)
- available = await cls.get_available_amount(db, user_id)
-
- if not settings_obj.enabled:
- logger.info("Попытка создать заявку на вывод при отключенной функции")
- return None
-
- if available < settings_obj.min_amount_kopeks:
- logger.info(
- "Недостаточно средств для вывода: user=%s, available=%s, min=%s",
- user_id,
- available,
- settings_obj.min_amount_kopeks,
- )
- return None
-
- return await create_referral_withdrawal_request(
- db=db,
- user_id=user_id,
- amount_kopeks=available,
- requisites=requisites,
- )
-
- @classmethod
- async def list_requests(
- cls, db: AsyncSession, status: Optional[str] = None, limit: int = 50
- ):
- return await get_referral_withdrawal_requests(db, status=status, limit=limit)
-
- @classmethod
- async def get_request(
- cls, db: AsyncSession, request_id: int
- ) -> Optional[ReferralWithdrawalRequest]:
- return await get_referral_withdrawal_request_by_id(db, request_id)
-
- @classmethod
- async def close_request(
- cls, db: AsyncSession, request_id: int, closed_by_id: Optional[int]
- ) -> Optional[ReferralWithdrawalRequest]:
- request = await get_referral_withdrawal_request_by_id(db, request_id)
- if not request:
- return None
- if request.status == "closed":
- return request
-
- if not request.user:
- logger.error(
- "Нельзя закрыть заявку %s: пользователь не найден", request_id
- )
- return None
-
- debited = await subtract_user_balance(
- db=db,
- user=request.user,
- amount_kopeks=request.amount_kopeks,
- description="Вывод реферального дохода",
- create_transaction=True,
- )
-
- if not debited:
- logger.warning(
- "Не удалось списать баланс для заявки на вывод партнёрки: %s", request_id
- )
- return None
- return await close_referral_withdrawal_request(
- db=db, request=request, closed_by_id=closed_by_id
- )
-
- @staticmethod
- def _parse_bool(value: Optional[str], default: bool) -> bool:
- if value is None:
- return bool(default)
- return str(value).strip().lower() in {"1", "true", "yes", "y"}
-
- @staticmethod
- def _parse_int(value: Optional[str], default: int) -> int:
- try:
- return int(value)
- except Exception:
- return int(default)
-
- @staticmethod
- def parse_amount_to_kopeks(raw: str) -> Optional[int]:
- try:
- cleaned = raw.replace(" ", "").replace(",", ".")
- amount = float(cleaned)
- if amount <= 0:
- return None
- return int(amount * 100)
- except Exception:
- return None
-
diff --git a/app/states.py b/app/states.py
index 21a10aa3..897297d3 100644
--- a/app/states.py
+++ b/app/states.py
@@ -35,10 +35,6 @@ class PromoCodeStates(StatesGroup):
waiting_for_code = State()
waiting_for_referral_code = State()
-
-class ReferralWithdrawalStates(StatesGroup):
- waiting_for_requisites = State()
-
class AdminStates(StatesGroup):
waiting_for_user_search = State()
@@ -101,9 +97,6 @@ class AdminStates(StatesGroup):
editing_user_traffic = State()
editing_user_referrals = State()
editing_user_referral_percent = State()
- editing_referral_withdraw_min_amount = State()
- editing_referral_withdraw_prompt_text = State()
- editing_referral_withdraw_success_text = State()
editing_rules_page = State()
editing_privacy_policy = State()