feat: implement referral payment system and enhance ticket management

This commit is contained in:
2026-04-18 22:34:15 +07:00
parent 3d19d9189b
commit 78a0ef9750
14 changed files with 165 additions and 31 deletions

View File

@@ -31,8 +31,11 @@ COPY --from=builder /install /usr/local
# Copy application source code
COPY . .
RUN sed -i 's/\r$//' ./entrypoints/startup.sh && \
chmod +x ./entrypoints/startup.sh
# Ensure the startup script is executable
RUN chmod +x entrypoints/startup.sh
RUN chmod +x ./entrypoints/startup.sh
# Default command runs linting, migrations, and starts the bot
CMD ["entrypoints/startup.sh"]
CMD ["sh", "./entrypoints/startup.sh"]

View File

@@ -16,8 +16,8 @@ services:
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-malenia} -d ${POSTGRES_DB:-malenia_db}",
]
interval: 5s
timeout: 10s
interval: 10s
timeout: 5s
retries: 5
bot:

22
entrypoints/dev.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -e
echo "[+] Formatting and checking..."
echo
ruff check --fix
black .
echo "[+] checking psql availability"
if ! pg_isready -h localhost -p 5432 ; then
echo "psql is down." >&2
exit 1
fi
echo "[+] psql is available!"
echo
echo "[+] Running migrations..."
echo
alembic upgrade head
echo "[+] Starting bot..."
python main.py

View File

@@ -1,19 +1,6 @@
#!/bin/bash
set -e
echo "[+] Formatting and checking..."
echo
ruff check --fix
black .
echo "[+] checking psql availability"
if ! pg_isready -h localhost -p 5432 ; then
echo "psql is down." >&2
exit 1
fi
echo "[+] psql is available!"
echo
echo "[+] Running migrations..."
echo
alembic upgrade head

View File

@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from keyboards.admins import promotion_confirmation
from keyboards.client import build_return_menu
from keyboards.client import close_kb
from schemas.di import DependenciesDTO
from schemas.promotions import NewPromotionContext
from states.admins import AdminStorage
@@ -28,7 +28,7 @@ async def push_cmd(msg: Message, state: FSMContext):
ctx = NewPromotionContext(creator_id=msg.from_user.id)
await msg.delete()
await msg.answer(ADMIN_PROMOTION_INIT, reply_markup=build_return_menu("delete", ""))
await msg.answer(ADMIN_PROMOTION_INIT, reply_markup=close_kb)
await state.set_state(AdminStorage.promotion_msg)
await state.set_data({"ctx": ctx})

View File

@@ -6,10 +6,19 @@ from aiogram.utils.deep_linking import create_start_link
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from keyboards.client import referals_kb
from keyboards.client import close_kb, referals_kb
from misc.utils import format_share_deep_link
from schemas.common import GeneralMessageContext
from schemas.di import DependenciesDTO
from texts import GENERAL_PROCESSING, REFERALS_MENU
from states.admins import AdminStorage
from texts import (
GENERAL_PROCESSING,
INVALID_ARGUMENT_REFPAY,
REFERALS_MENU,
SPECIFY_REFERAL_PAYMENT_AMOUNT,
SUCCESSFUL_REFPAY,
USER_NO_REFERAL,
)
router = Router()
@@ -45,17 +54,70 @@ async def refpay(
user = await deps.user_service.fetch_user_by_thread(session, msg.message_thread_id)
referal = await deps.user_repository.get_user_by_id(session, user.referal_id)
if not referal:
await status_msg.edit_text("У пользователя нет реферала.")
await status_msg.edit_text(USER_NO_REFERAL)
return
amount = command.args
if not amount.isdigit():
await status_msg.edit_text(
"❌ Укажите в аргументах количество средств, которые необходимо перевести рефералу пользователя."
)
await status_msg.edit_text(INVALID_ARGUMENT_REFPAY)
return
amount = int(amount)
await deps.user_repository.increase_user_balance(session, referal.id, amount)
await status_msg.edit_text(f"<b>💸 Баланс реферала ({referal.id}) пополнен на {amount}₽</b>")
await status_msg.edit_text(SUCCESSFUL_REFPAY.format(referal_id=referal.id, amount=amount))
@router.callback_query(F.message.chat.id == settings.admin_group_id, F.data == "refpay")
async def refpay_cb(
cb: CallbackQuery,
state: FSMContext,
session: AsyncSession,
deps: DependenciesDTO,
):
await state.clear()
user = await deps.user_service.fetch_user_by_thread(session, cb.message.message_thread_id)
referal = await deps.user_repository.get_user_by_id(session, user.referal_id)
if not referal:
await cb.message.answer(USER_NO_REFERAL, reply_markup=close_kb)
return
msg = await cb.message.answer(
SPECIFY_REFERAL_PAYMENT_AMOUNT, reply_markup=close_kb
)
ctx = GeneralMessageContext(msg=msg)
await state.set_state(AdminStorage.refpay_amount)
await state.set_data({"ctx": ctx})
@router.message(
AdminStorage.refpay_amount,
F.chat.id == settings.admin_group_id,
F.from_user.is_bot == False, # noqa: E712
)
async def refpay_execute(
msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
data = await state.get_data()
await state.clear()
await msg.delete()
ctx: GeneralMessageContext | None = data.get("ctx")
if ctx:
status_msg = ctx.msg
else:
status_msg = await msg.answer(GENERAL_PROCESSING)
amount = msg.text
if not amount.isdigit():
await status_msg.edit_text(INVALID_ARGUMENT_REFPAY, reply_markup=close_kb)
return
user = await deps.user_service.fetch_user_by_thread(session, msg.message_thread_id)
referal = await deps.user_repository.get_user_by_id(session, user.referal_id)
amount = int(amount)
await deps.user_repository.increase_user_balance(session, referal.id, amount)
await status_msg.edit_text(SUCCESSFUL_REFPAY.format(referal_id=referal.id, amount=amount))

View File

@@ -79,3 +79,12 @@ async def close_ticket(
await state.clear()
await deps.user_service.close_ticket(session, msg)
@router.callback_query(F.message.chat.id == settings.admin_group_id, F.data == "close")
async def close_ticket_cb(
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
await state.clear()
await deps.user_service.close_ticket(session, cb.message)

View File

@@ -7,3 +7,10 @@ promotion_confirmation: InlineKeyboardMarkup = InlineKeyboardBuilder(
[InlineKeyboardButton(text="", callback_data="decline")],
]
).as_markup()
ticket_actions: InlineKeyboardMarkup = InlineKeyboardBuilder(
[
[InlineKeyboardButton(text="🔒", callback_data="close")],
[InlineKeyboardButton(text="💸 Пополнить баланс реферала", callback_data="refpay")],
]
).as_markup()

View File

@@ -33,6 +33,7 @@ payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
]
).as_markup()
close_kb: InlineKeyboardMarkup = InlineKeyboardBuilder([[InlineKeyboardButton(text="", callback_data="delete")]]).as_markup()
def _return_btn(data: str = "menu:main", text: str = "⬅️ Назад") -> InlineKeyboardButton:
return InlineKeyboardButton(text=text, callback_data=data)

View File

@@ -131,6 +131,12 @@ def format_username(username: str | None) -> str:
return texts.NO_USERNAME
def format_referal_id(referal_id: int | None) -> str:
if referal_id:
return str(referal_id)
return texts.NOTHING_PLACEHOLDER
def convert_duration_to_int(duration: SubscriptionDuration) -> int:
mapping: dict[str, int] = {
SubscriptionDuration.MONTH.value: 1,
@@ -187,6 +193,7 @@ def format_support_subscription(
user_id: int,
balance: int,
username: str | None = None,
referal_id: int | None = None,
) -> str:
username = format_username(username)
whitelist_description = "ВЫКЛ"
@@ -204,6 +211,7 @@ def format_support_subscription(
whitelist_description=whitelist_description,
price=calculate_price(subscription),
balance=balance,
referal_id=referal_id,
)

8
schemas/common.py Normal file
View File

@@ -0,0 +1,8 @@
from dataclasses import dataclass
from aiogram.types import Message
@dataclass
class GeneralMessageContext:
msg: Message

View File

@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from db.models.tickets import TicketStatus
from db.models.users import User
from keyboards.admins import ticket_actions
from misc import utils
from repositories import UserRepository
from repositories.tickets import TicketRepository
@@ -51,9 +52,14 @@ class UserService:
@staticmethod
async def format_admin_support_message(
ticket_number: int, user: TelegramUser, rw_user: RWUserInfo | None, balance: int
ticket_number: int,
user: TelegramUser,
rw_user: RWUserInfo | None,
balance: int,
referal_id: int | None = None,
):
username_display = utils.format_username(user.username)
referal_display = utils.format_referal_id(referal_id)
if rw_user is None:
# Пользователь не найден в Remnawave
@@ -62,6 +68,8 @@ class UserService:
full_name=user.full_name,
username_display=username_display,
telegram_id=user.id,
referal_id=referal_display,
balance=balance,
)
status_icon, status_text = utils.format_status(rw_user.status)
@@ -85,6 +93,7 @@ class UserService:
hwid_limit=hwid_limit,
squads=squads,
remnawave_uuid=rw_user.uuid,
referal_id=referal_display,
)
async def add_user(
@@ -126,7 +135,9 @@ class UserService:
user_id=user.id,
username=user.username,
balance=db_user.balance,
referal_id=db_user.referal_id,
),
reply_markup=ticket_actions,
)
return
except Exception:
@@ -159,7 +170,9 @@ class UserService:
user_id=user.id,
username=user.username,
balance=db_user.balance,
referal_id=db_user.referal_id,
),
reply_markup=ticket_actions,
)
async def support_forward_message(self, session: AsyncSession, msg: Message, rw: RemnawaveSDK):
@@ -205,7 +218,9 @@ class UserService:
user=msg.from_user,
rw_user=rw_user,
balance=user.balance,
referal_id=user.referal_id,
),
reply_markup=ticket_actions,
)
await msg.copy_to(settings.admin_group_id, message_thread_id=thread.message_thread_id)
@@ -230,6 +245,8 @@ class UserService:
async def close_ticket(self, session: AsyncSession, msg: Message):
thread_id = msg.message_thread_id
if not thread_id:
return
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
await msg.bot.edit_forum_topic(
@@ -247,9 +264,14 @@ class UserService:
async def update_user_link(
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
) -> User | None:
) -> User:
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
user = await self.user_repository.get_user_by_id(session, user_id)
if not rw_user:
return user
link = utils.get_subscription_link(rw_user.short_uuid)
user = await self.user_repository.update_user_link(session, user_id, link)

View File

@@ -3,3 +3,4 @@ from aiogram.fsm.state import State, StatesGroup
class AdminStorage(StatesGroup):
promotion_msg = State()
refpay_amount = State()

View File

@@ -163,6 +163,7 @@ _SUPPORT_USER_DESCRIPTION = (
"👤 <b>Профиль:</b> {full_name}\n"
"🔗 <b>Alias:</b> {username_display}\n"
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
"❤️ <b>Referal UID:</b> <code>{referal_id}</code>\n\n"
"💰 <b>Баланс:</b> <code>{balance}</code>"
)
@@ -202,9 +203,8 @@ ADMIN_TICKET_WITH_RW = (
ADMIN_TICKET_WITHOUT_RW = (
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
"👤 <b>Имя:</b> {full_name}\n"
"🔗 <b>Alias:</b> {username_display}\n"
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
+ _SUPPORT_USER_DESCRIPTION
+ "\n"
+ _DIVIDER
+ "⚠️ <b>Remnawave-профиль отсутствует</b>"
)
@@ -219,3 +219,7 @@ ADMIN_STANDARD_FALLBACK = "<b>❌ Ошибка выполнения.</b> Про
ADMIN_PROMOTION_INIT = "👥 Введите текст рассылки"
ADMIN_PROMOTION_CONFIRMATION = "❓ Отправить сообщение всем пользователям бота?"
SUCCESSFULLY_SENT_PROMOTION = "Успешно разосланно {user_count} пользователям."
SPECIFY_REFERAL_PAYMENT_AMOUNT = "Укажите количество средств для перевода рефералу пользователя."
USER_NO_REFERAL = "У пользователя нет реферала."
INVALID_ARGUMENT_REFPAY = "❌ Укажите в аргументах количество средств, которые необходимо перевести рефералу пользователя."
SUCCESSFUL_REFPAY = "<b>💸 Баланс реферала ({referal_id}) пополнен на {amount}₽</b>"