Files
remnawave-bedolaga-telegram…/app/services/subscription_checkout_service.py
T
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00

75 lines
2.2 KiB
Python

import structlog
from sqlalchemy.exc import MissingGreenlet
from app.database.models import Subscription, User
from app.utils.cache import UserCache
logger = structlog.get_logger(__name__)
_CHECKOUT_SESSION_KEY = 'subscription_checkout'
_CHECKOUT_TTL_SECONDS = 3600
async def save_subscription_checkout_draft(user_id: int, data: dict, ttl: int = _CHECKOUT_TTL_SECONDS) -> bool:
"""Persist subscription checkout draft data in cache."""
return await UserCache.set_user_session(user_id, _CHECKOUT_SESSION_KEY, data, ttl)
async def get_subscription_checkout_draft(user_id: int) -> dict | None:
"""Retrieve subscription checkout draft from cache."""
return await UserCache.get_user_session(user_id, _CHECKOUT_SESSION_KEY)
async def clear_subscription_checkout_draft(user_id: int) -> bool:
"""Remove stored subscription checkout draft for the user."""
return await UserCache.delete_user_session(user_id, _CHECKOUT_SESSION_KEY)
async def has_subscription_checkout_draft(user_id: int) -> bool:
draft = await get_subscription_checkout_draft(user_id)
return draft is not None
def should_offer_checkout_resume(
user: User,
has_draft: bool,
*,
subscription: Subscription | None = None,
) -> bool:
"""
Determine whether checkout resume button should be available for the user.
Only users without an active paid subscription or users currently on trial
are eligible to continue assembling the subscription from the saved draft.
"""
if not has_draft:
return False
if subscription is None:
try:
subscription = getattr(user, 'subscription', None)
except MissingGreenlet as error:
logger.warning(
'Не удалось лениво загрузить подписку пользователя при проверке возврата к checkout',
getattr=getattr(user, 'id', None),
error=error,
)
subscription = None
if subscription is None:
return True
if getattr(subscription, 'is_trial', False):
return True
if getattr(subscription, 'actual_status', None) == 'expired':
return True
return False