mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-13 07:30:28 +00:00
Merge pull request #2012 from BEDOLAGA-DEV/revert-2011-rrpx8s-bedolaga/add-referral-income-withdrawal-feature
Revert "Debit balance when closing referral withdrawals"
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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", "💸 <b>Вывод партнёрки</b>")
|
||||
+ "\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"💸 <b>Заявка #{request.id}</b>\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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -621,25 +621,6 @@
|
||||
"ADMIN_STATISTICS": "📊 Statistics",
|
||||
"ADMIN_STATS_BUTTON": "📊 Statistics",
|
||||
"ADMIN_STATS_REFERRALS": "🤝 Referrals",
|
||||
"ADMIN_REFERRAL_WITHDRAWALS": "💸 Referral payouts",
|
||||
"ADMIN_REFERRAL_WITHDRAWALS_TITLE": "💸 <b>Referral payouts</b>",
|
||||
"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": "🔗 <b>Your referral link:</b>",
|
||||
"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": "👥 <b>Your referrals</b> (page {current}/{total})",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Activity: {days} days ago",
|
||||
|
||||
@@ -621,25 +621,6 @@
|
||||
"ADMIN_STATISTICS": "📊 Статистика",
|
||||
"ADMIN_STATS_BUTTON": "📊 Статистика",
|
||||
"ADMIN_STATS_REFERRALS": "🤝 Партнерка",
|
||||
"ADMIN_REFERRAL_WITHDRAWALS": "💸 Вывод партнёрки",
|
||||
"ADMIN_REFERRAL_WITHDRAWALS_TITLE": "💸 <b>Вывод партнёрки</b>",
|
||||
"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": "🔗 <b>Ваша реферальная ссылка:</b>",
|
||||
"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": "👥 <b>Ваши рефералы</b> (стр. {current}/{total})",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Активность: {days} дн. назад",
|
||||
|
||||
@@ -617,29 +617,10 @@
|
||||
"ADMIN_SQUAD_MIGRATION_TITLE": "🚚 <b>Переїзд сквадів</b>",
|
||||
"ADMIN_SQUAD_REMOVE_ALL": "❌ Видалити всіх користувачів",
|
||||
"ADMIN_SQUAD_RENAME": "✏️ Перейменувати",
|
||||
"ADMIN_STATISTICS": "📊 Статистика",
|
||||
"ADMIN_STATS_BUTTON": "📊 Статистика",
|
||||
"ADMIN_STATS_REFERRALS": "🤝 Партнерка",
|
||||
"ADMIN_REFERRAL_WITHDRAWALS": "💸 Виведення партнерки",
|
||||
"ADMIN_REFERRAL_WITHDRAWALS_TITLE": "💸 <b>Виведення партнерки</b>",
|
||||
"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🎯 <b>Запрошення до VPN сервісу</b>\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": "🔗 <b>Ваше реферальне посилання:</b>",
|
||||
"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": "👥 <b>Ваші реферали</b> (стор. {current}/{total})",
|
||||
"REFERRAL_LINK_CAPTION": "🔗 Ваше реферальне посилання:\n{link}",
|
||||
"REFERRAL_LINK_TITLE": "🔗 <b>Ваше реферальне посилання:</b>",
|
||||
"REFERRAL_LIST_BUTTON": "👥 Список рефералів",
|
||||
"REFERRAL_LIST_EMPTY": "📋 У вас поки немає рефералів.\n\nПоділіться своїм реферальним посиланням, щоб почати заробляти!",
|
||||
"REFERRAL_LIST_HEADER": "👥 <b>Ваші реферали</b> (стор. {current}/{total})",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Активність: {days} дн. тому",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO": " 🕐 Активність: давно",
|
||||
"REFERRAL_LIST_ITEM_EARNED": " 💎 Зароблено з нього: {amount}",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user