- 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
110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
from datetime import datetime, timezone
|
|
|
|
from loguru import logger
|
|
|
|
from app.models.payment import Payment, PaymentStatus
|
|
from app.repositories.payment import PaymentRepository
|
|
from app.repositories.tariff import TariffRepository
|
|
from app.repositories.user import UserRepository
|
|
|
|
|
|
class PaymentService:
|
|
def __init__(
|
|
self,
|
|
payment_repo: PaymentRepository,
|
|
user_repo: UserRepository,
|
|
tariff_repo: TariffRepository,
|
|
):
|
|
self.payment_repo = payment_repo
|
|
self.user_repo = user_repo
|
|
self.tariff_repo = tariff_repo
|
|
|
|
async def create(
|
|
self,
|
|
user_id: int,
|
|
tariff_id: int,
|
|
provider: str,
|
|
amount: float,
|
|
currency: str = "RUB",
|
|
external_id: str | None = None,
|
|
) -> Payment:
|
|
user = await self.user_repo.get(user_id)
|
|
if user is None:
|
|
raise ValueError(f"User not found: {user_id}")
|
|
|
|
tariff = await self.tariff_repo.get(tariff_id)
|
|
if tariff is None:
|
|
raise ValueError(f"Tariff not found: {tariff_id}")
|
|
|
|
payment = await self.payment_repo.create(
|
|
user_id=user_id,
|
|
tariff_id=tariff_id,
|
|
amount=amount,
|
|
currency=currency,
|
|
status=PaymentStatus.PENDING,
|
|
provider=provider,
|
|
external_id=external_id,
|
|
)
|
|
|
|
logger.info("Payment created: id={} user={} amount={}", payment.id, user_id, amount)
|
|
return payment
|
|
|
|
async def confirm(
|
|
self, payment_id: int, external_id: str | None = None
|
|
) -> Payment:
|
|
payment = await self.payment_repo.get(payment_id)
|
|
if payment is None:
|
|
raise ValueError(f"Payment not found: {payment_id}")
|
|
|
|
if payment.status != PaymentStatus.PENDING:
|
|
raise ValueError(
|
|
f"Cannot confirm payment {payment_id}: "
|
|
f"current status is {payment.status.value}"
|
|
)
|
|
|
|
payment = await self.payment_repo.update(
|
|
payment_id,
|
|
status=PaymentStatus.CONFIRMED,
|
|
paid_at=datetime.now(timezone.utc),
|
|
external_id=external_id or payment.external_id,
|
|
)
|
|
|
|
logger.info("Payment confirmed: id={} user={}", payment_id, payment.user_id)
|
|
return payment
|
|
|
|
async def fail(self, payment_id: int) -> Payment:
|
|
payment = await self.payment_repo.get(payment_id)
|
|
if payment is None:
|
|
raise ValueError(f"Payment not found: {payment_id}")
|
|
|
|
payment = await self.payment_repo.update(
|
|
payment_id, status=PaymentStatus.FAILED
|
|
)
|
|
|
|
logger.info("Payment failed: id={} user={}", payment_id, payment.user_id)
|
|
return payment
|
|
|
|
async def refund(self, payment_id: int) -> Payment:
|
|
payment = await self.payment_repo.get(payment_id)
|
|
if payment is None:
|
|
raise ValueError(f"Payment not found: {payment_id}")
|
|
|
|
payment = await self.payment_repo.update(
|
|
payment_id, status=PaymentStatus.REFUNDED
|
|
)
|
|
|
|
logger.info("Payment refunded: id={} user={}", payment_id, payment.user_id)
|
|
return payment
|
|
|
|
async def get_by_id(self, payment_id: int) -> Payment | None:
|
|
return await self.payment_repo.get(payment_id)
|
|
|
|
async def get_by_external_id(self, external_id: str) -> Payment | None:
|
|
return await self.payment_repo.get_by_external_id(external_id)
|
|
|
|
async def get_user_payments(self, user_id: int) -> list[Payment]:
|
|
return await self.payment_repo.get_by_user_id(user_id)
|
|
|
|
async def get_pending(self) -> list[Payment]:
|
|
return await self.payment_repo.get_pending_payments()
|