feat: add subscription and transaction repositories

- Implemented SubscriptionRepository with methods to get, create, and update subscriptions.
- Added BalanceTXRepository for creating and retrieving balance transactions.
- Introduced a test script for simulating Pally webhook for local testing.
This commit is contained in:
2026-04-27 21:33:25 +07:00
parent 049f31118d
commit 287f8eebda
34 changed files with 3401 additions and 53 deletions

View File

@@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta
from uuid import UUID
from remnawave import RemnawaveSDK
from remnawave.models import UpdateUserRequestDto
from remnawave.models import CreateUserRequestDto, UpdateUserRequestDto
logger = logging.getLogger(__name__)
@@ -319,3 +319,33 @@ async def update_squads(
exc,
)
return False
async def create_user(
sdk: RemnawaveSDK,
username: str,
telegram_id: int,
expire_at: datetime,
hwid_device_limit: int | None,
squad_uuids: list[str],
) -> RWUserInfo | None:
"""
Создать нового пользователя в Remnawave.
hwid_device_limit=None → без ограничений.
Возвращает RWUserInfo при успехе, None при ошибке.
"""
try:
dto = CreateUserRequestDto(
username=username,
telegram_id=telegram_id,
expire_at=expire_at,
hwid_device_limit=hwid_device_limit,
active_internal_squads=[UUID(s) for s in squad_uuids] if squad_uuids else None,
)
result = await sdk.users.create_user(dto)
if result is None:
return None
return _parse_user(result)
except Exception as exc:
logger.warning("Failed to create user username=%s: %s", username, exc)
return None

View File

@@ -1,6 +1,7 @@
import datetime
import logging
from aiogram.types import Message
from aiogram.types import Message, User as TelegramUser
from remnawave import RemnawaveSDK
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,8 +13,10 @@ from bot.texts import (
from db.models.users import User
from misc import utils
from repositories import UserRepository
from repositories.subscriptions import SubscriptionRepository
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
from schemas.users import NewUserDTO
from services.rw import RWUserInfo, get_user_by_telegram_id
from services import rw
logger = logging.getLogger(__name__)
@@ -22,12 +25,15 @@ class UserServiceError(Exception): ...
class UserService:
def __init__(self, user_repository: UserRepository) -> None:
def __init__(
self, user_repository: UserRepository, subscription_repository: SubscriptionRepository
) -> None:
self.user_repository = user_repository
self.subscription_repository = subscription_repository
@staticmethod
def format_subscription_message(
rw_user: RWUserInfo | None,
rw_user: rw.RWUserInfo | None,
link: str | None = None,
):
if rw_user is None:
@@ -62,7 +68,7 @@ class UserService:
if user:
return user
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
rw_user = await rw.get_user_by_telegram_id(rw_sdk, user_id)
link = utils.get_subscription_link(rw_user.short_uuid) if rw_user else None
referal = int(referal) if str(referal).isdigit() and user_id != int(referal) else None
@@ -72,7 +78,7 @@ class UserService:
async def update_user_link(
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
) -> User | None:
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
rw_user = await rw.get_user_by_telegram_id(rw_sdk, user_id)
user = await self.user_repository.get_user_by_id(session, user_id)
@@ -97,3 +103,98 @@ class UserService:
logger.critical("failed to send a message to user %d.", user.id, exc_info=True)
return sent
async def buy_subscription(
self,
session: AsyncSession,
subscription: SubscriptionPlan,
rw_sdk: RemnawaveSDK,
user: TelegramUser,
):
duration = utils.convert_duration_to_timedelta(subscription.duration)
squads = [InternalSquads.DEFAULT]
if subscription.whitelists:
squads.append(InternalSquads.WHITELISTS)
## If subscription exists, add.
rw_user = await rw.get_user_by_telegram_id(rw_sdk, user.id)
if rw_user:
db_subscription = await self.subscription_repository.get_subscription_by_user_id(
session, user.id
)
if db_subscription:
db_subscription = await self.subscription_repository.update_sub(
session,
user.id,
end_date=rw_user.expire_at + duration,
autorenew=False,
status=SubscriptionStatus.ACTIVE,
devices=int(subscription.devices),
whitelists=subscription.whitelists,
plan_price_paid=utils.calculate_price(subscription),
duration_days=duration.days,
)
else:
db_subscription = await self.subscription_repository.create_subscription(
session,
user_id=user.id,
end_date=rw_user.expire_at + duration,
autorenew=False,
status=SubscriptionStatus.ACTIVE,
devices=int(subscription.devices),
whitelists=subscription.whitelists,
plan_price_paid=utils.calculate_price(subscription),
duration_days=duration.days,
)
await rw.add_days(rw_sdk, rw_user.uuid, duration.days)
await rw.update_squads(rw_sdk, rw_user.uuid, squads)
await rw.set_hwid_limit(rw_sdk, rw_user.uuid, subscription.devices)
await self.update_user_link(session, user.id, rw_sdk)
return db_subscription
# Create
expire = datetime.datetime.now(datetime.UTC) + duration
username = "tg_" + str(user.username or user.id)
rw_user = await rw.create_user(
rw_sdk,
username=username,
telegram_id=user.id,
expire_at=expire,
hwid_device_limit=subscription.devices,
squad_uuids=squads,
)
db_subscription = await self.subscription_repository.get_subscription_by_user_id(
session, user.id
)
if db_subscription:
db_subscription = await self.subscription_repository.update_sub(
session,
user.id,
end_date=expire,
autorenew=False,
status=SubscriptionStatus.ACTIVE,
devices=int(subscription.devices),
whitelists=subscription.whitelists,
plan_price_paid=utils.calculate_price(subscription),
duration_days=duration.days,
)
else:
db_subscription = await self.subscription_repository.create_subscription(
session,
user_id=user.id,
end_date=expire,
autorenew=False,
status=SubscriptionStatus.ACTIVE,
devices=int(subscription.devices),
whitelists=subscription.whitelists,
plan_price_paid=utils.calculate_price(subscription),
duration_days=duration.days,
)
await self.update_user_link(session, user.id, rw_sdk)
return db_subscription