- FastAPI REST API with JWT auth - aiogram 3 Telegram bot with admin middleware - APScheduler daily tasks (expiry, reminders, revoke, sync) - SQLAlchemy 2 async ORM with Alembic migrations - Jinja2 admin panel (Dashboard, Users, Payments, Servers, Tariffs) - VPN provider abstraction with MockProvider - Stats service with revenue/subscription analytics - Docker Compose (PostgreSQL + Redis + app) - Healthcheck endpoint
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import func, select, text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.payment import Payment, PaymentStatus
|
|
from app.models.tariff import Tariff
|
|
from app.models.user import User
|
|
from app.repositories.payment import PaymentRepository
|
|
from app.repositories.tariff import TariffRepository
|
|
from app.repositories.user import UserRepository
|
|
from app.services.billing import BillingService
|
|
|
|
|
|
@dataclass
|
|
class SystemStats:
|
|
total_users: int = 0
|
|
active_users: int = 0
|
|
expired_subscriptions: int = 0
|
|
revenue_month: float = 0.0
|
|
revenue_year: float = 0.0
|
|
avg_subscription_price: float = 0.0
|
|
|
|
|
|
class StatsService:
|
|
def __init__(self, session: AsyncSession):
|
|
self.session = session
|
|
|
|
async def get_stats(self) -> SystemStats:
|
|
total_users = await self._count_users()
|
|
active_users = await self._count_active_users()
|
|
expired_subscriptions = await self._count_expired()
|
|
revenue_month = await self._sum_revenue_since(self._start_of_month())
|
|
revenue_year = await self._sum_revenue_since(self._start_of_year())
|
|
avg_subscription_price = await self._avg_tariff_price()
|
|
|
|
return SystemStats(
|
|
total_users=total_users,
|
|
active_users=active_users,
|
|
expired_subscriptions=expired_subscriptions,
|
|
revenue_month=round(revenue_month, 2),
|
|
revenue_year=round(revenue_year, 2),
|
|
avg_subscription_price=round(avg_subscription_price, 2),
|
|
)
|
|
|
|
async def _count_users(self) -> int:
|
|
stmt = select(func.count(User.id))
|
|
return (await self.session.execute(stmt)).scalar_one()
|
|
|
|
async def _count_active_users(self) -> int:
|
|
stmt = select(func.count(User.id)).where(User.is_active.is_(True))
|
|
return (await self.session.execute(stmt)).scalar_one()
|
|
|
|
async def _count_expired(self) -> int:
|
|
user_repo = UserRepository(self.session)
|
|
tariff_repo = TariffRepository(self.session)
|
|
payment_repo = PaymentRepository(self.session)
|
|
|
|
billing = BillingService(payment_repo, tariff_repo, user_repo)
|
|
expired_ids = await billing.expire_subscriptions()
|
|
return len(set(expired_ids))
|
|
|
|
async def _sum_revenue_since(self, since: datetime) -> float:
|
|
stmt = (
|
|
select(func.coalesce(func.sum(Payment.amount), 0))
|
|
.where(Payment.status == PaymentStatus.CONFIRMED)
|
|
.where(Payment.paid_at >= since)
|
|
)
|
|
return float((await self.session.execute(stmt)).scalar_one())
|
|
|
|
async def _avg_tariff_price(self) -> float:
|
|
stmt = select(func.coalesce(func.avg(Tariff.price), 0))
|
|
return float((await self.session.execute(stmt)).scalar_one())
|
|
|
|
@staticmethod
|
|
def _start_of_month() -> datetime:
|
|
now = datetime.now(timezone.utc)
|
|
return now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
@staticmethod
|
|
def _start_of_year() -> datetime:
|
|
now = datetime.now(timezone.utc)
|
|
return now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|