Files
smolkik-code 7c7c88621d Initial commit: VPN Control Panel
- 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
2026-07-05 17:50:11 +07:00

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)