335be66980
- Add MULTI_TARIFF_ENABLED feature flag for gradual rollout
- Migration 0041: remove unique constraint on subscriptions.user_id
- Migration 0042: add remnawave_short_id (NOT NULL, UNIQUE) to subscriptions
- Each subscription gets its own Remnawave user (user_{tg_id}_{short_id})
- Add _resolve_subscription() to all 30+ bot handlers for per-subscription routing
- Add my_subscriptions.py with list/detail views and delegation handlers
- Refactor cabinet subscription.py (4687 lines -> 10 focused modules)
- Add subscription_id parameter to all cabinet API endpoints
- Adapt all services: autopay, wheel, contests, campaign, guest purchase,
monitoring, blocked users, account merge, promo codes, payments
- Replace all user.subscription (singular) with user.subscriptions iteration
- IDOR protection via get_subscription_by_id_for_user on all endpoints
- Full backward compatibility: MULTI_TARIFF_ENABLED=False = legacy behavior
84 lines
3.8 KiB
Python
84 lines
3.8 KiB
Python
from collections.abc import Awaitable, Callable
|
|
from datetime import UTC, datetime, timedelta
|
|
from typing import Any
|
|
|
|
import structlog
|
|
from aiogram import BaseMiddleware
|
|
from aiogram.types import TelegramObject
|
|
|
|
from app.database.models import SubscriptionStatus
|
|
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
# Буфер времени перед деактивацией (защита от race condition при продлении)
|
|
EXPIRATION_BUFFER_MINUTES = 5
|
|
|
|
|
|
class SubscriptionStatusMiddleware(BaseMiddleware):
|
|
"""
|
|
Проверяет статус подписки пользователя.
|
|
ВАЖНО: Использует db и db_user из data, которые уже загружены в AuthMiddleware.
|
|
Не создаёт дополнительных сессий БД.
|
|
|
|
Деактивирует подписку только если она истекла более чем на EXPIRATION_BUFFER_MINUTES минут.
|
|
Это защищает от race conditions при продлении подписки.
|
|
"""
|
|
|
|
async def __call__(
|
|
self,
|
|
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
|
event: TelegramObject,
|
|
data: dict[str, Any],
|
|
) -> Any:
|
|
# Используем db и user из AuthMiddleware - не создаём новую сессию!
|
|
db = data.get('db')
|
|
user = data.get('db_user')
|
|
|
|
if db and user and getattr(user, 'subscriptions', None):
|
|
try:
|
|
current_time = datetime.now(UTC)
|
|
needs_commit = False
|
|
|
|
# Check all subscriptions (multi-tariff aware)
|
|
for subscription in user.subscriptions:
|
|
# Суточные подписки управляются DailySubscriptionService — не экспайрим их тут
|
|
tariff = getattr(subscription, 'tariff', None)
|
|
is_active_daily = tariff and getattr(tariff, 'is_daily', False) and not subscription.is_daily_paused
|
|
|
|
if (
|
|
subscription.status == SubscriptionStatus.ACTIVE.value
|
|
and subscription.end_date
|
|
and subscription.end_date <= current_time
|
|
and not is_active_daily
|
|
):
|
|
time_since_expiry = current_time - subscription.end_date
|
|
|
|
if time_since_expiry > timedelta(minutes=EXPIRATION_BUFFER_MINUTES):
|
|
subscription.status = SubscriptionStatus.EXPIRED.value
|
|
subscription.updated_at = current_time
|
|
needs_commit = True
|
|
|
|
logger.warning(
|
|
'⏰ Middleware DEACTIVATION: подписка (user_id=) деактивирована. end_date=, просрочена на',
|
|
subscription_id=subscription.id,
|
|
user_id=user.id,
|
|
end_date=subscription.end_date,
|
|
time_since_expiry=time_since_expiry,
|
|
)
|
|
else:
|
|
logger.debug(
|
|
'⏰ Middleware: подписка пользователя истекла недавно ждём буфер мин',
|
|
user_id=user.id,
|
|
time_since_expiry=time_since_expiry,
|
|
EXPIRATION_BUFFER_MINUTES=EXPIRATION_BUFFER_MINUTES,
|
|
)
|
|
|
|
if needs_commit:
|
|
await db.commit()
|
|
|
|
except Exception as e:
|
|
logger.error('Ошибка проверки статуса подписки', error=e)
|
|
|
|
return await handler(event, data)
|