- 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
135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
|
|
from aiogram import Router
|
|
from aiogram.filters import Command, CommandObject
|
|
from aiogram.types import Message
|
|
from loguru import logger
|
|
|
|
from app.bot.texts import (
|
|
user_info,
|
|
USER_NOT_FOUND,
|
|
INVALID_ARGS,
|
|
NO_DATA,
|
|
expired_list,
|
|
expiring_list,
|
|
)
|
|
from app.database import async_session_factory
|
|
from app.models.payment import PaymentStatus
|
|
from app.repositories.payment import PaymentRepository
|
|
from app.repositories.tariff import TariffRepository
|
|
from app.repositories.user import UserRepository
|
|
from app.services.billing import BillingService
|
|
|
|
router = Router(name="subscription")
|
|
|
|
|
|
@router.message(Command("renew"))
|
|
async def cmd_renew(message: Message, command: CommandObject) -> None:
|
|
if not command.args or not command.args.strip().isdigit():
|
|
await message.answer(
|
|
INVALID_ARGS.format("/renew <telegram_id>"),
|
|
parse_mode="HTML",
|
|
)
|
|
return
|
|
|
|
telegram_id = int(command.args.strip())
|
|
|
|
async with async_session_factory() as session:
|
|
user_repo = UserRepository(session)
|
|
user = await user_repo.get_by_telegram_id(telegram_id)
|
|
|
|
if user is None:
|
|
await message.answer(
|
|
USER_NOT_FOUND.format(telegram_id), parse_mode="HTML"
|
|
)
|
|
return
|
|
|
|
billing = BillingService(
|
|
PaymentRepository(session),
|
|
TariffRepository(session),
|
|
user_repo,
|
|
)
|
|
sub = await billing.get_active_subscription(user.id)
|
|
|
|
await message.answer(
|
|
user_info(
|
|
user_id=user.id,
|
|
telegram_id=user.telegram_id,
|
|
username=user.username,
|
|
full_name=user.full_name,
|
|
is_active=user.is_active,
|
|
created_at=user.created_at.strftime("%Y-%m-%d"),
|
|
subscription_active=sub is not None and sub.is_active,
|
|
tariff_name=sub.tariff.name if sub and sub.tariff else None,
|
|
remaining_days=sub.remaining_days if sub else 0,
|
|
),
|
|
parse_mode="HTML",
|
|
)
|
|
|
|
|
|
@router.message(Command("expired"))
|
|
async def cmd_expired(message: Message) -> None:
|
|
async with async_session_factory() as session:
|
|
payment_repo = PaymentRepository(session)
|
|
tariff_repo = TariffRepository(session)
|
|
user_repo = UserRepository(session)
|
|
|
|
confirmed = await payment_repo.get_by_status(PaymentStatus.CONFIRMED)
|
|
now = datetime.now(timezone.utc)
|
|
items = []
|
|
|
|
for payment in confirmed:
|
|
tariff = await tariff_repo.get(payment.tariff_id)
|
|
if tariff is None:
|
|
continue
|
|
start = payment.paid_at or payment.created_at
|
|
end = start + timedelta(days=tariff.duration_days)
|
|
if now > end:
|
|
user = await user_repo.get(payment.user_id)
|
|
items.append(
|
|
{
|
|
"user_id": payment.user_id,
|
|
"telegram_id": user.telegram_id if user else "?",
|
|
"tariff_name": tariff.name,
|
|
}
|
|
)
|
|
|
|
await message.answer(expired_list(items), parse_mode="HTML")
|
|
|
|
|
|
@router.message(Command("expiring"))
|
|
async def cmd_expiring(message: Message, command: CommandObject) -> None:
|
|
days = 3
|
|
if command.args and command.args.strip().isdigit():
|
|
days = int(command.args.strip())
|
|
|
|
async with async_session_factory() as session:
|
|
payment_repo = PaymentRepository(session)
|
|
tariff_repo = TariffRepository(session)
|
|
user_repo = UserRepository(session)
|
|
|
|
confirmed = await payment_repo.get_by_status(PaymentStatus.CONFIRMED)
|
|
now = datetime.now(timezone.utc)
|
|
items = []
|
|
|
|
for payment in confirmed:
|
|
tariff = await tariff_repo.get(payment.tariff_id)
|
|
if tariff is None:
|
|
continue
|
|
start = payment.paid_at or payment.created_at
|
|
end = start + timedelta(days=tariff.duration_days)
|
|
remaining = (end - now).days
|
|
if 0 <= remaining <= days:
|
|
user = await user_repo.get(payment.user_id)
|
|
items.append(
|
|
{
|
|
"user_id": payment.user_id,
|
|
"telegram_id": user.telegram_id if user else "?",
|
|
"tariff_name": tariff.name,
|
|
"remaining_days": remaining,
|
|
}
|
|
)
|
|
|
|
items.sort(key=lambda x: x["remaining_days"])
|
|
await message.answer(expiring_list(items, days), parse_mode="HTML")
|