mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-15 08:30:29 +00:00
607 lines
19 KiB
Python
607 lines
19 KiB
Python
import logging
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional, List, Tuple
|
||
from sqlalchemy import select, and_, func
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy.orm import selectinload
|
||
|
||
from app.database.models import (
|
||
Subscription, SubscriptionStatus, User,
|
||
SubscriptionServer
|
||
)
|
||
from app.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
async def get_subscription_by_user_id(db: AsyncSession, user_id: int) -> Optional[Subscription]:
|
||
result = await db.execute(
|
||
select(Subscription)
|
||
.options(selectinload(Subscription.user))
|
||
.where(Subscription.user_id == user_id)
|
||
.order_by(Subscription.created_at.desc())
|
||
.limit(1)
|
||
)
|
||
subscription = result.scalar_one_or_none()
|
||
|
||
if subscription:
|
||
subscription = await check_and_update_subscription_status(db, subscription)
|
||
|
||
return subscription
|
||
|
||
|
||
async def create_trial_subscription(
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
duration_days: int = None,
|
||
traffic_limit_gb: int = None,
|
||
device_limit: int = None,
|
||
squad_uuid: str = None
|
||
) -> Subscription:
|
||
|
||
duration_days = duration_days or settings.TRIAL_DURATION_DAYS
|
||
traffic_limit_gb = traffic_limit_gb or settings.TRIAL_TRAFFIC_LIMIT_GB
|
||
device_limit = device_limit or settings.TRIAL_DEVICE_LIMIT
|
||
squad_uuid = squad_uuid or settings.TRIAL_SQUAD_UUID
|
||
|
||
end_date = datetime.utcnow() + timedelta(days=duration_days)
|
||
|
||
subscription = Subscription(
|
||
user_id=user_id,
|
||
status=SubscriptionStatus.ACTIVE.value,
|
||
is_trial=True,
|
||
start_date=datetime.utcnow(),
|
||
end_date=end_date,
|
||
traffic_limit_gb=traffic_limit_gb,
|
||
device_limit=device_limit,
|
||
connected_squads=[squad_uuid] if squad_uuid else []
|
||
)
|
||
|
||
db.add(subscription)
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"🎁 Создана триальная подписка для пользователя {user_id}")
|
||
return subscription
|
||
|
||
|
||
async def create_paid_subscription(
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
duration_days: int,
|
||
traffic_limit_gb: int = 0,
|
||
device_limit: int = 1,
|
||
connected_squads: List[str] = None
|
||
) -> Subscription:
|
||
|
||
end_date = datetime.utcnow() + timedelta(days=duration_days)
|
||
|
||
subscription = Subscription(
|
||
user_id=user_id,
|
||
status=SubscriptionStatus.ACTIVE.value,
|
||
is_trial=False,
|
||
start_date=datetime.utcnow(),
|
||
end_date=end_date,
|
||
traffic_limit_gb=traffic_limit_gb,
|
||
device_limit=device_limit,
|
||
connected_squads=connected_squads or []
|
||
)
|
||
|
||
db.add(subscription)
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"💎 Создана платная подписка для пользователя {user_id}")
|
||
return subscription
|
||
|
||
|
||
async def extend_subscription(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
days: int
|
||
) -> Subscription:
|
||
current_time = datetime.utcnow()
|
||
|
||
logger.info(f"🔄 Продление подписки {subscription.id} на {days} дней")
|
||
logger.info(f"📊 Текущие параметры: статус={subscription.status}, окончание={subscription.end_date}")
|
||
|
||
if subscription.end_date > current_time:
|
||
subscription.end_date = subscription.end_date + timedelta(days=days)
|
||
logger.info(f"📅 Подписка активна, добавляем {days} дней к текущей дате окончания")
|
||
else:
|
||
subscription.end_date = current_time + timedelta(days=days)
|
||
logger.info(f"📅 Подписка истекла, устанавливаем новую дату окончания")
|
||
|
||
if subscription.status == SubscriptionStatus.EXPIRED.value:
|
||
subscription.status = SubscriptionStatus.ACTIVE.value
|
||
logger.info(f"🔄 Статус изменён с EXPIRED на ACTIVE")
|
||
|
||
subscription.updated_at = current_time
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"✅ Подписка продлена до: {subscription.end_date}")
|
||
logger.info(f"📊 Новые параметры: статус={subscription.status}, окончание={subscription.end_date}")
|
||
|
||
return subscription
|
||
|
||
|
||
async def add_subscription_traffic(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
gb: int
|
||
) -> Subscription:
|
||
|
||
subscription.add_traffic(gb)
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"📈 К подписке пользователя {subscription.user_id} добавлено {gb} ГБ трафика")
|
||
return subscription
|
||
|
||
|
||
async def add_subscription_devices(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
devices: int
|
||
) -> Subscription:
|
||
|
||
subscription.device_limit += devices
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"📱 К подписке пользователя {subscription.user_id} добавлено {devices} устройств")
|
||
return subscription
|
||
|
||
|
||
async def add_subscription_squad(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
squad_uuid: str
|
||
) -> Subscription:
|
||
|
||
if squad_uuid not in subscription.connected_squads:
|
||
subscription.connected_squads = subscription.connected_squads + [squad_uuid]
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"🌍 К подписке пользователя {subscription.user_id} добавлен сквад {squad_uuid}")
|
||
|
||
return subscription
|
||
|
||
|
||
async def remove_subscription_squad(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
squad_uuid: str
|
||
) -> Subscription:
|
||
|
||
if squad_uuid in subscription.connected_squads:
|
||
squads = subscription.connected_squads.copy()
|
||
squads.remove(squad_uuid)
|
||
subscription.connected_squads = squads
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"🚫 Из подписки пользователя {subscription.user_id} удален сквад {squad_uuid}")
|
||
|
||
return subscription
|
||
|
||
|
||
async def update_subscription_autopay(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
enabled: bool,
|
||
days_before: int = 3
|
||
) -> Subscription:
|
||
|
||
subscription.autopay_enabled = enabled
|
||
subscription.autopay_days_before = days_before
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
status = "включен" if enabled else "выключен"
|
||
logger.info(f"💳 Автоплатеж для подписки пользователя {subscription.user_id} {status}")
|
||
return subscription
|
||
|
||
|
||
async def deactivate_subscription(
|
||
db: AsyncSession,
|
||
subscription: Subscription
|
||
) -> Subscription:
|
||
|
||
subscription.status = SubscriptionStatus.DISABLED.value
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"❌ Подписка пользователя {subscription.user_id} деактивирована")
|
||
return subscription
|
||
|
||
|
||
async def get_expiring_subscriptions(
|
||
db: AsyncSession,
|
||
days_before: int = 3
|
||
) -> List[Subscription]:
|
||
|
||
threshold_date = datetime.utcnow() + timedelta(days=days_before)
|
||
|
||
result = await db.execute(
|
||
select(Subscription)
|
||
.options(selectinload(Subscription.user))
|
||
.where(
|
||
and_(
|
||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||
Subscription.end_date <= threshold_date,
|
||
Subscription.end_date > datetime.utcnow()
|
||
)
|
||
)
|
||
)
|
||
return result.scalars().all()
|
||
|
||
|
||
async def get_expired_subscriptions(db: AsyncSession) -> List[Subscription]:
|
||
|
||
result = await db.execute(
|
||
select(Subscription)
|
||
.options(selectinload(Subscription.user))
|
||
.where(
|
||
and_(
|
||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||
Subscription.end_date <= datetime.utcnow()
|
||
)
|
||
)
|
||
)
|
||
return result.scalars().all()
|
||
|
||
|
||
async def get_subscriptions_for_autopay(db: AsyncSession) -> List[Subscription]:
|
||
current_time = datetime.utcnow()
|
||
|
||
result = await db.execute(
|
||
select(Subscription)
|
||
.options(selectinload(Subscription.user))
|
||
.where(
|
||
and_(
|
||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||
Subscription.autopay_enabled == True,
|
||
Subscription.is_trial == False
|
||
)
|
||
)
|
||
)
|
||
all_autopay_subscriptions = result.scalars().all()
|
||
|
||
ready_for_autopay = []
|
||
for subscription in all_autopay_subscriptions:
|
||
days_until_expiry = (subscription.end_date - current_time).days
|
||
|
||
if days_until_expiry <= subscription.autopay_days_before and subscription.end_date > current_time:
|
||
ready_for_autopay.append(subscription)
|
||
|
||
return ready_for_autopay
|
||
|
||
|
||
async def get_subscriptions_statistics(db: AsyncSession) -> dict:
|
||
|
||
total_result = await db.execute(select(func.count(Subscription.id)))
|
||
total_subscriptions = total_result.scalar()
|
||
|
||
active_result = await db.execute(
|
||
select(func.count(Subscription.id))
|
||
.where(Subscription.status == SubscriptionStatus.ACTIVE.value)
|
||
)
|
||
active_subscriptions = active_result.scalar()
|
||
|
||
trial_result = await db.execute(
|
||
select(func.count(Subscription.id))
|
||
.where(
|
||
and_(
|
||
Subscription.is_trial == True,
|
||
Subscription.status == SubscriptionStatus.ACTIVE.value
|
||
)
|
||
)
|
||
)
|
||
trial_subscriptions = trial_result.scalar()
|
||
|
||
paid_subscriptions = active_subscriptions - trial_subscriptions
|
||
|
||
today = datetime.utcnow().date()
|
||
today_result = await db.execute(
|
||
select(func.count(Subscription.id))
|
||
.where(
|
||
and_(
|
||
Subscription.created_at >= today,
|
||
Subscription.is_trial == False
|
||
)
|
||
)
|
||
)
|
||
purchased_today = today_result.scalar()
|
||
|
||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||
week_result = await db.execute(
|
||
select(func.count(Subscription.id))
|
||
.where(
|
||
and_(
|
||
Subscription.created_at >= week_ago,
|
||
Subscription.is_trial == False
|
||
)
|
||
)
|
||
)
|
||
purchased_week = week_result.scalar()
|
||
|
||
month_ago = datetime.utcnow() - timedelta(days=30)
|
||
month_result = await db.execute(
|
||
select(func.count(Subscription.id))
|
||
.where(
|
||
and_(
|
||
Subscription.created_at >= month_ago,
|
||
Subscription.is_trial == False
|
||
)
|
||
)
|
||
)
|
||
purchased_month = month_result.scalar()
|
||
|
||
return {
|
||
"total_subscriptions": total_subscriptions,
|
||
"active_subscriptions": active_subscriptions,
|
||
"trial_subscriptions": trial_subscriptions,
|
||
"paid_subscriptions": paid_subscriptions,
|
||
"purchased_today": purchased_today,
|
||
"purchased_week": purchased_week,
|
||
"purchased_month": purchased_month
|
||
}
|
||
|
||
|
||
async def update_subscription_usage(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
used_gb: float
|
||
) -> Subscription:
|
||
subscription.traffic_used_gb = used_gb
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
return subscription
|
||
|
||
async def get_all_subscriptions(
|
||
db: AsyncSession,
|
||
page: int = 1,
|
||
limit: int = 10
|
||
) -> Tuple[List[Subscription], int]:
|
||
count_result = await db.execute(
|
||
select(func.count(Subscription.id))
|
||
)
|
||
total_count = count_result.scalar()
|
||
|
||
offset = (page - 1) * limit
|
||
|
||
result = await db.execute(
|
||
select(Subscription)
|
||
.options(selectinload(Subscription.user))
|
||
.order_by(Subscription.created_at.desc())
|
||
.offset(offset)
|
||
.limit(limit)
|
||
)
|
||
|
||
subscriptions = result.scalars().all()
|
||
|
||
return subscriptions, total_count
|
||
|
||
async def add_subscription_servers(
|
||
db: AsyncSession,
|
||
subscription: Subscription,
|
||
server_squad_ids: List[int],
|
||
paid_prices: List[int] = None
|
||
) -> Subscription:
|
||
|
||
if paid_prices is None:
|
||
paid_prices = [0] * len(server_squad_ids)
|
||
|
||
for i, server_id in enumerate(server_squad_ids):
|
||
subscription_server = SubscriptionServer(
|
||
subscription_id=subscription.id,
|
||
server_squad_id=server_id,
|
||
paid_price_kopeks=paid_prices[i] if i < len(paid_prices) else 0
|
||
)
|
||
db.add(subscription_server)
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"🌍 К подписке {subscription.id} добавлено {len(server_squad_ids)} серверов")
|
||
return subscription
|
||
|
||
async def get_subscription_server_ids(
|
||
db: AsyncSession,
|
||
subscription_id: int
|
||
) -> List[int]:
|
||
|
||
result = await db.execute(
|
||
select(SubscriptionServer.server_squad_id)
|
||
.where(SubscriptionServer.subscription_id == subscription_id)
|
||
)
|
||
return [row[0] for row in result.fetchall()]
|
||
|
||
|
||
async def get_subscription_servers(
|
||
db: AsyncSession,
|
||
subscription_id: int
|
||
) -> List[dict]:
|
||
|
||
from app.database.models import ServerSquad
|
||
|
||
result = await db.execute(
|
||
select(SubscriptionServer, ServerSquad)
|
||
.join(ServerSquad, SubscriptionServer.server_squad_id == ServerSquad.id)
|
||
.where(SubscriptionServer.subscription_id == subscription_id)
|
||
)
|
||
|
||
servers_info = []
|
||
for sub_server, server_squad in result.fetchall():
|
||
servers_info.append({
|
||
'server_id': server_squad.id,
|
||
'squad_uuid': server_squad.squad_uuid,
|
||
'display_name': server_squad.display_name,
|
||
'country_code': server_squad.country_code,
|
||
'paid_price_kopeks': sub_server.paid_price_kopeks,
|
||
'connected_at': sub_server.connected_at,
|
||
'is_available': server_squad.is_available
|
||
})
|
||
|
||
return servers_info
|
||
|
||
async def remove_subscription_servers(
|
||
db: AsyncSession,
|
||
subscription_id: int,
|
||
server_squad_ids: List[int]
|
||
) -> bool:
|
||
try:
|
||
from app.database.models import SubscriptionServer
|
||
from sqlalchemy import delete
|
||
|
||
await db.execute(
|
||
delete(SubscriptionServer)
|
||
.where(
|
||
SubscriptionServer.subscription_id == subscription_id,
|
||
SubscriptionServer.server_squad_id.in_(server_squad_ids)
|
||
)
|
||
)
|
||
|
||
await db.commit()
|
||
logger.info(f"🗑️ Удалены серверы {server_squad_ids} из подписки {subscription_id}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка удаления серверов из подписки: {e}")
|
||
await db.rollback()
|
||
return False
|
||
|
||
|
||
async def get_subscription_renewal_cost(
|
||
db: AsyncSession,
|
||
subscription_id: int,
|
||
period_days: int
|
||
) -> int:
|
||
try:
|
||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES, settings
|
||
|
||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||
|
||
servers_info = await get_subscription_servers(db, subscription_id)
|
||
servers_cost = sum(server_info['paid_price_kopeks'] for server_info in servers_info)
|
||
|
||
subscription = await db.get(Subscription, subscription_id)
|
||
if not subscription:
|
||
return base_price
|
||
|
||
traffic_cost = 0
|
||
if subscription.traffic_limit_gb > 0:
|
||
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
|
||
|
||
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
|
||
|
||
total_cost = base_price + servers_cost + traffic_cost + devices_cost
|
||
|
||
logger.info(f"💰 Расчет продления подписки {subscription_id} на {period_days} дней:")
|
||
logger.info(f" 📅 Период: {base_price/100}₽")
|
||
logger.info(f" 🌍 Серверы: {servers_cost/100}₽")
|
||
logger.info(f" 📊 Трафик: {traffic_cost/100}₽")
|
||
logger.info(f" 📱 Устройства: {devices_cost/100}₽")
|
||
logger.info(f" 💎 ИТОГО: {total_cost/100}₽")
|
||
|
||
return total_cost
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка расчета стоимости продления: {e}")
|
||
from app.config import PERIOD_PRICES
|
||
return PERIOD_PRICES.get(period_days, 0)
|
||
|
||
async def expire_subscription(
|
||
db: AsyncSession,
|
||
subscription: Subscription
|
||
) -> Subscription:
|
||
|
||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||
subscription.updated_at = datetime.utcnow()
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"⏰ Подписка пользователя {subscription.user_id} помечена как истёкшая")
|
||
return subscription
|
||
|
||
|
||
async def check_and_update_subscription_status(
|
||
db: AsyncSession,
|
||
subscription: Subscription
|
||
) -> Subscription:
|
||
|
||
current_time = datetime.utcnow()
|
||
|
||
if (subscription.status == SubscriptionStatus.ACTIVE.value and
|
||
subscription.end_date <= current_time):
|
||
|
||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||
subscription.updated_at = current_time
|
||
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"⏰ Статус подписки пользователя {subscription.user_id} изменен на 'expired'")
|
||
|
||
return subscription
|
||
|
||
async def create_subscription(
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
status: str = "trial",
|
||
is_trial: bool = True,
|
||
end_date: datetime = None,
|
||
traffic_limit_gb: int = 10,
|
||
traffic_used_gb: float = 0.0,
|
||
device_limit: int = 1,
|
||
connected_squads: list = None,
|
||
remnawave_short_uuid: str = None,
|
||
subscription_url: str = ""
|
||
) -> Subscription:
|
||
|
||
if end_date is None:
|
||
end_date = datetime.utcnow() + timedelta(days=3)
|
||
|
||
if connected_squads is None:
|
||
connected_squads = []
|
||
|
||
subscription = Subscription(
|
||
user_id=user_id,
|
||
status=status,
|
||
is_trial=is_trial,
|
||
end_date=end_date,
|
||
traffic_limit_gb=traffic_limit_gb,
|
||
traffic_used_gb=traffic_used_gb,
|
||
device_limit=device_limit,
|
||
connected_squads=connected_squads,
|
||
remnawave_short_uuid=remnawave_short_uuid,
|
||
subscription_url=subscription_url
|
||
)
|
||
|
||
db.add(subscription)
|
||
await db.commit()
|
||
await db.refresh(subscription)
|
||
|
||
logger.info(f"✅ Создана подписка для пользователя {user_id}")
|
||
return subscription
|