From fbb1091f8be1251ca16b6f5f91ac88d55e828921 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 24 Nov 2025 07:29:57 +0300 Subject: [PATCH] Revert "Debit balance when closing referral withdrawals" --- app/config.py | 4 - app/database/crud/referral_withdrawal.py | 98 ----- app/database/models.py | 23 -- app/database/universal_migration.py | 90 ---- app/handlers/admin/referrals.py | 431 +------------------- app/handlers/referral.py | 144 +------ app/keyboards/inline.py | 23 +- app/localization/locales/en.json | 25 -- app/localization/locales/ru.json | 25 -- app/localization/locales/ua.json | 43 +- app/services/referral_withdrawal_service.py | 278 ------------- app/states.py | 7 - 12 files changed, 18 insertions(+), 1173 deletions(-) delete mode 100644 app/database/crud/referral_withdrawal.py delete mode 100644 app/services/referral_withdrawal_service.py 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()