Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 7bc29c3421 deps(deps): bump the python-dependencies group across 1 directory with 10 updates
---
updated-dependencies:
- dependency-name: aiogram
  dependency-version: 3.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: qrcode[pil]
  dependency-version: '8.2'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: aiohttp
  dependency-version: 3.13.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: pydantic
  dependency-version: 2.12.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: pydantic-settings
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: python-dotenv
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: uvicorn
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: python-multipart
  dependency-version: 0.0.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: apscheduler
  dependency-version: 3.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: email-validator
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 04:34:17 +00:00
303 changed files with 6750 additions and 7247 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
+3 -3
View File
@@ -10,11 +10,11 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7 - uses: astral-sh/setup-uv@v5
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
with: with:
python-version: '3.13' python-version: '3.13'
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
".": "3.12.1" ".": "3.12.0"
} }
-10
View File
@@ -1,15 +1,5 @@
# Changelog # Changelog
## [3.12.1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.12.0...v3.12.1) (2026-02-16)
### Bug Fixes
* add /start burst rate-limit to prevent spam abuse ([61a9722](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/61a97220d30031816ab23e33a46717e4895c0758))
* add promo code anti-abuse protections ([97ec39a](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/97ec39aa803f0e3f03fdcd482df0cbcb86fd1efd))
* handle TelegramBadRequest in ticket edit_message_text calls ([8e61fe4](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/8e61fe47746da2ac09c3ea8c4dbfc6be198e49e3))
* replace deprecated Query(regex=) with pattern= ([871ceb8](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/871ceb866ccf1f3a770c7ef33406e1a43d0a7ff7))
## [3.12.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.11.0...v3.12.0) (2026-02-15) ## [3.12.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.11.0...v3.12.0) (2026-02-15)
+1 -1
View File
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
FROM python:3.13-slim FROM python:3.13-slim
ARG VERSION="v3.12.1" # x-release-please-version ARG VERSION="v3.12.0" # x-release-please-version
ARG BUILD_DATE ARG BUILD_DATE
ARG VCS_REF ARG VCS_REF
+13 -16
View File
@@ -1,5 +1,6 @@
import logging
import redis.asyncio as redis import redis.asyncio as redis
import structlog
from aiogram import Bot, Dispatcher, types from aiogram import Bot, Dispatcher, types
from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.storage.redis import RedisStorage from aiogram.fsm.storage.redis import RedisStorage
@@ -62,7 +63,6 @@ from app.handlers.stars_payments import register_stars_handlers
from app.middlewares.auth import AuthMiddleware from app.middlewares.auth import AuthMiddleware
from app.middlewares.blacklist import BlacklistMiddleware from app.middlewares.blacklist import BlacklistMiddleware
from app.middlewares.button_stats import ButtonStatsMiddleware from app.middlewares.button_stats import ButtonStatsMiddleware
from app.middlewares.context_binding import ContextVarsMiddleware
from app.middlewares.global_error import GlobalErrorMiddleware from app.middlewares.global_error import GlobalErrorMiddleware
from app.middlewares.logging import LoggingMiddleware from app.middlewares.logging import LoggingMiddleware
from app.middlewares.maintenance import MaintenanceMiddleware from app.middlewares.maintenance import MaintenanceMiddleware
@@ -75,14 +75,14 @@ from app.utils.message_patch import patch_message_methods
patch_message_methods() patch_message_methods()
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def debug_callback_handler(callback: types.CallbackQuery): async def debug_callback_handler(callback: types.CallbackQuery):
logger.info('🔍 DEBUG CALLBACK:') logger.info('🔍 DEBUG CALLBACK:')
logger.info('Data', callback_data=callback.data) logger.info(f' - Data: {callback.data}')
logger.info('User', from_user_id=callback.from_user.id) logger.info(f' - User: {callback.from_user.id}')
logger.info('Username', username=callback.from_user.username) logger.info(f' - Username: {callback.from_user.username}')
async def setup_bot() -> tuple[Bot, Dispatcher]: async def setup_bot() -> tuple[Bot, Dispatcher]:
@@ -90,7 +90,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
await cache.connect() await cache.connect()
logger.info('Кеш инициализирован') logger.info('Кеш инициализирован')
except Exception as e: except Exception as e:
logger.warning('Кеш не инициализирован', error=e) logger.warning(f'Кеш не инициализирован: {e}')
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
@@ -106,15 +106,12 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
storage = RedisStorage(redis_client) storage = RedisStorage(redis_client)
logger.info('Подключено к Redis для FSM storage') logger.info('Подключено к Redis для FSM storage')
except Exception as e: except Exception as e:
logger.warning('Не удалось подключиться к Redis', error=e) logger.warning(f'Не удалось подключиться к Redis: {e}')
logger.info('Используется MemoryStorage для FSM') logger.info('Используется MemoryStorage для FSM')
storage = MemoryStorage() storage = MemoryStorage()
dp = Dispatcher(storage=storage) dp = Dispatcher(storage=storage)
dp.message.middleware(ContextVarsMiddleware())
dp.callback_query.middleware(ContextVarsMiddleware())
dp.pre_checkout_query.middleware(ContextVarsMiddleware())
dp.message.middleware(GlobalErrorMiddleware()) dp.message.middleware(GlobalErrorMiddleware())
dp.callback_query.middleware(GlobalErrorMiddleware()) dp.callback_query.middleware(GlobalErrorMiddleware())
dp.pre_checkout_query.middleware(GlobalErrorMiddleware()) dp.pre_checkout_query.middleware(GlobalErrorMiddleware())
@@ -208,7 +205,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
await maintenance_service.start_monitoring() await maintenance_service.start_monitoring()
logger.info('Мониторинг техработ запущен') logger.info('Мониторинг техработ запущен')
except Exception as e: except Exception as e:
logger.error('Ошибка запуска мониторинга техработ', error=e) logger.error(f'Ошибка запуска мониторинга техработ: {e}')
else: else:
logger.info('Мониторинг техработ отключен настройками') logger.info('Мониторинг техработ отключен настройками')
@@ -239,7 +236,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
'Установите MINIAPP_CUSTOM_URL.' 'Установите MINIAPP_CUSTOM_URL.'
) )
elif settings.is_cabinet_mode(): elif settings.is_cabinet_mode():
logger.info('🏠 Режим Cabinet активен, базовый URL', MINIAPP_CUSTOM_URL=settings.MINIAPP_CUSTOM_URL) logger.info(f'🏠 Режим Cabinet активен, базовый URL: {settings.MINIAPP_CUSTOM_URL}')
# Load per-section button styles cache # Load per-section button styles cache
if settings.is_cabinet_mode(): if settings.is_cabinet_mode():
@@ -248,7 +245,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
await load_button_styles_cache() await load_button_styles_cache()
except Exception as e: except Exception as e:
logger.warning('Failed to load button styles cache', error=e) logger.warning(f'Failed to load button styles cache: {e}')
logger.info('Бот успешно настроен') logger.info('Бот успешно настроен')
@@ -260,10 +257,10 @@ async def shutdown_bot():
await maintenance_service.stop_monitoring() await maintenance_service.stop_monitoring()
logger.info('Мониторинг техработ остановлен') logger.info('Мониторинг техработ остановлен')
except Exception as e: except Exception as e:
logger.error('Ошибка остановки мониторинга', error=e) logger.error(f'Ошибка остановки мониторинга: {e}')
try: try:
await cache.close() await cache.close()
logger.info('Соединения с кешем закрыты') logger.info('Соединения с кешем закрыты')
except Exception as e: except Exception as e:
logger.error('Ошибка закрытия кеша', error=e) logger.error(f'Ошибка закрытия кеша: {e}')
+2 -2
View File
@@ -1,18 +1,18 @@
"""OAuth 2.0 provider implementations for cabinet authentication.""" """OAuth 2.0 provider implementations for cabinet authentication."""
import logging
import secrets import secrets
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, TypedDict from typing import Any, TypedDict
import httpx import httpx
import structlog
from pydantic import BaseModel from pydantic import BaseModel
from app.config import settings from app.config import settings
from app.utils.cache import cache, cache_key from app.utils.cache import cache, cache_key
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
STATE_TTL_SECONDS = 600 # 10 minutes STATE_TTL_SECONDS = 600 # 10 minutes
+4 -6
View File
@@ -1,8 +1,8 @@
"""FastAPI dependencies for cabinet module.""" """FastAPI dependencies for cabinet module."""
import asyncio import asyncio
import logging
import structlog
from aiogram import Bot from aiogram import Bot
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -18,7 +18,7 @@ from app.services.maintenance_service import maintenance_service
from .auth.jwt_handler import get_token_payload from .auth.jwt_handler import get_token_payload
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
security = HTTPBearer(auto_error=False) security = HTTPBearer(auto_error=False)
@@ -161,12 +161,10 @@ async def get_current_cabinet_user(
except HTTPException: except HTTPException:
raise raise
except TimeoutError: except TimeoutError:
logger.warning('Timeout checking channel subscription for user', telegram_id=user.telegram_id) logger.warning(f'Timeout checking channel subscription for user {user.telegram_id}')
# Don't block user if check times out # Don't block user if check times out
except Exception as e: except Exception as e:
logger.warning( logger.warning(f'Failed to check channel subscription for user {user.telegram_id}: {e}')
'Failed to check channel subscription for user', telegram_id=user.telegram_id, error=e
)
# Don't block user if check fails # Don't block user if check fails
return user return user
+12 -19
View File
@@ -1,9 +1,9 @@
"""Admin routes for managing VPN applications in app-config.json.""" """Admin routes for managing VPN applications in app-config.json."""
import json import json
import logging
from pathlib import Path from pathlib import Path
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +16,7 @@ from app.services.system_settings_service import bot_configuration_service
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/apps', tags=['Cabinet Admin Apps']) router = APIRouter(prefix='/admin/apps', tags=['Cabinet Admin Apps'])
@@ -231,7 +231,7 @@ async def create_app(
config['platforms'] = platforms config['platforms'] = platforms
_save_config(config) _save_config(config)
logger.info('Admin created app for platform', admin_id=admin.id, app_id=request.app.id, platform=platform) logger.info(f"Admin {admin.id} created app '{request.app.id}' for platform '{platform}'")
return request.app return request.app
@@ -274,7 +274,7 @@ async def update_app(
config['platforms'] = platforms config['platforms'] = platforms
_save_config(config) _save_config(config)
logger.info('Admin updated app in platform', admin_id=admin.id, app_id=app_id, platform=platform) logger.info(f"Admin {admin.id} updated app '{app_id}' in platform '{platform}'")
return request.app return request.app
@@ -310,7 +310,7 @@ async def delete_app(
config['platforms'] = platforms config['platforms'] = platforms
_save_config(config) _save_config(config)
logger.info('Admin deleted app from platform', admin_id=admin.id, app_id=app_id, platform=platform) logger.info(f"Admin {admin.id} deleted app '{app_id}' from platform '{platform}'")
return {'status': 'deleted', 'app_id': app_id} return {'status': 'deleted', 'app_id': app_id}
@@ -355,7 +355,7 @@ async def reorder_apps(
config['platforms'] = platforms config['platforms'] = platforms
_save_config(config) _save_config(config)
logger.info('Admin reordered apps in platform', admin_id=admin.id, platform=platform) logger.info(f"Admin {admin.id} reordered apps in platform '{platform}'")
return {'status': 'reordered', 'order': request.app_ids} return {'status': 'reordered', 'order': request.app_ids}
@@ -374,7 +374,7 @@ async def update_branding(
config['config']['branding'] = request.branding.model_dump() config['config']['branding'] = request.branding.model_dump()
_save_config(config) _save_config(config)
logger.info('Admin updated branding', admin_id=admin.id) logger.info(f'Admin {admin.id} updated branding')
return request.branding return request.branding
@@ -434,14 +434,7 @@ async def copy_app_to_platform(
config['platforms'] = platforms config['platforms'] = platforms
_save_config(config) _save_config(config)
logger.info( logger.info(f"Admin {admin.id} copied app '{app_id}' from '{platform}' to '{target_platform}' as '{new_id}'")
'Admin copied app from to as',
admin_id=admin.id,
app_id=app_id,
platform=platform,
target_platform=target_platform,
new_id=new_id,
)
return {'status': 'copied', 'new_id': new_id, 'target_platform': target_platform} return {'status': 'copied', 'new_id': new_id, 'target_platform': target_platform}
@@ -505,9 +498,9 @@ async def set_remnawave_config_uuid(
try: try:
await bot_configuration_service.set_value(db, 'CABINET_REMNA_SUB_CONFIG', uuid_value) await bot_configuration_service.set_value(db, 'CABINET_REMNA_SUB_CONFIG', uuid_value)
await db.commit() await db.commit()
logger.info('Admin updated CABINET_REMNA_SUB_CONFIG to', admin_id=admin.id, uuid_value=uuid_value) logger.info(f"Admin {admin.id} updated CABINET_REMNA_SUB_CONFIG to '{uuid_value}'")
except Exception as e: except Exception as e:
logger.error('Error saving RemnaWave config UUID', error=e) logger.error(f'Error saving RemnaWave config UUID: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to save configuration', detail='Failed to save configuration',
@@ -554,7 +547,7 @@ async def get_remnawave_subscription_config(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Error fetching RemnaWave config', error=e) logger.error(f'Error fetching RemnaWave config: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Failed to fetch config from RemnaWave: {e!s}', detail=f'Failed to fetch config from RemnaWave: {e!s}',
@@ -579,7 +572,7 @@ async def list_remnawave_subscription_configs(
for c in configs for c in configs
] ]
except Exception as e: except Exception as e:
logger.error('Error listing RemnaWave configs', error=e) logger.error(f'Error listing RemnaWave configs: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Failed to fetch configs from RemnaWave: {e!s}', detail=f'Failed to fetch configs from RemnaWave: {e!s}',
+13 -15
View File
@@ -1,8 +1,8 @@
"""Admin routes for Ban System monitoring in cabinet.""" """Admin routes for Ban System monitoring in cabinet."""
import logging
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from app.config import settings from app.config import settings
@@ -45,7 +45,7 @@ from ..schemas.ban_system import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/ban-system', tags=['Cabinet Admin Ban System']) router = APIRouter(prefix='/admin/ban-system', tags=['Cabinet Admin Ban System'])
@@ -53,11 +53,9 @@ router = APIRouter(prefix='/admin/ban-system', tags=['Cabinet Admin Ban System']
def _get_ban_api() -> BanSystemAPI: def _get_ban_api() -> BanSystemAPI:
"""Get Ban System API instance.""" """Get Ban System API instance."""
logger.debug( logger.debug(
'Ban System check enabled: configured', f'Ban System check - enabled: {settings.is_ban_system_enabled()}, configured: {settings.is_ban_system_configured()}'
is_ban_system_enabled=settings.is_ban_system_enabled(),
is_ban_system_configured=settings.is_ban_system_configured(),
) )
logger.debug('Ban System URL', get_ban_system_api_url=settings.get_ban_system_api_url()) logger.debug(f'Ban System URL: {settings.get_ban_system_api_url()}')
if not settings.is_ban_system_enabled(): if not settings.is_ban_system_enabled():
raise HTTPException( raise HTTPException(
@@ -85,13 +83,13 @@ async def _api_request(api: BanSystemAPI, method: str, *args, **kwargs) -> Any:
func = getattr(api, method) func = getattr(api, method)
return await func(*args, **kwargs) return await func(*args, **kwargs)
except BanSystemAPIError as e: except BanSystemAPIError as e:
logger.error('Ban System API error', error=e) logger.error(f'Ban System API error: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail=f'Ban System API error: {e.message}', detail=f'Ban System API error: {e.message}',
) )
except Exception as e: except Exception as e:
logger.error('Ban System unexpected error', error=e) logger.error(f'Ban System unexpected error: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Internal error: {e!s}', detail=f'Internal error: {e!s}',
@@ -135,7 +133,7 @@ async def get_stats(
api = _get_ban_api() api = _get_ban_api()
data = await _api_request(api, 'get_stats') data = await _api_request(api, 'get_stats')
logger.debug('Ban System raw stats', data=data) logger.debug(f'Ban System raw stats: {data}')
# Extract punishment stats # Extract punishment stats
punishment_stats = data.get('punishment_stats') or {} punishment_stats = data.get('punishment_stats') or {}
@@ -366,7 +364,7 @@ async def unban_user(
api = _get_ban_api() api = _get_ban_api()
try: try:
await _api_request(api, 'enable_user', user_id=user_id) await _api_request(api, 'enable_user', user_id=user_id)
logger.info('Admin unbanned user in Ban System', admin_id=admin.id, user_id=user_id) logger.info(f'Admin {admin.id} unbanned user {user_id} in Ban System')
return UnbanResponse(success=True, message='User unbanned successfully') return UnbanResponse(success=True, message='User unbanned successfully')
except HTTPException: except HTTPException:
raise raise
@@ -389,7 +387,7 @@ async def ban_user(
minutes=request.minutes, minutes=request.minutes,
reason=request.reason, reason=request.reason,
) )
logger.info('Admin banned user', admin_id=admin.id, username=request.username, reason=request.reason) logger.info(f'Admin {admin.id} banned user {request.username}: {request.reason}')
return UnbanResponse(success=True, message='User banned successfully') return UnbanResponse(success=True, message='User banned successfully')
except HTTPException: except HTTPException:
raise raise
@@ -821,7 +819,7 @@ async def set_setting(
api = _get_ban_api() api = _get_ban_api()
data = await _api_request(api, 'set_setting', key=key, value=value) data = await _api_request(api, 'set_setting', key=key, value=value)
logger.info('Admin changed Ban System setting to', admin_id=admin.id, key=key, value=value) logger.info(f'Admin {admin.id} changed Ban System setting {key} to {value}')
return _parse_setting_response(key, data) return _parse_setting_response(key, data)
@@ -835,7 +833,7 @@ async def toggle_setting(
api = _get_ban_api() api = _get_ban_api()
data = await _api_request(api, 'toggle_setting', key=key) data = await _api_request(api, 'toggle_setting', key=key)
logger.info('Admin toggled Ban System setting', admin_id=admin.id, key=key) logger.info(f'Admin {admin.id} toggled Ban System setting {key}')
return _parse_setting_response(key, data, default_type='bool') return _parse_setting_response(key, data, default_type='bool')
@@ -852,7 +850,7 @@ async def whitelist_add(
api = _get_ban_api() api = _get_ban_api()
try: try:
await _api_request(api, 'whitelist_add', username=request.username) await _api_request(api, 'whitelist_add', username=request.username)
logger.info('Admin added to Ban System whitelist', admin_id=admin.id, username=request.username) logger.info(f'Admin {admin.id} added {request.username} to Ban System whitelist')
return UnbanResponse(success=True, message=f'User {request.username} added to whitelist') return UnbanResponse(success=True, message=f'User {request.username} added to whitelist')
except HTTPException: except HTTPException:
raise raise
@@ -869,7 +867,7 @@ async def whitelist_remove(
api = _get_ban_api() api = _get_ban_api()
try: try:
await _api_request(api, 'whitelist_remove', username=request.username) await _api_request(api, 'whitelist_remove', username=request.username)
logger.info('Admin removed from Ban System whitelist', admin_id=admin.id, username=request.username) logger.info(f'Admin {admin.id} removed {request.username} from Ban System whitelist')
return UnbanResponse(success=True, message=f'User {request.username} removed from whitelist') return UnbanResponse(success=True, message=f'User {request.username} removed from whitelist')
except HTTPException: except HTTPException:
raise raise
+10 -18
View File
@@ -1,8 +1,8 @@
"""Admin routes for broadcasts in cabinet.""" """Admin routes for broadcasts in cabinet."""
import logging
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import distinct, func, select from sqlalchemy import distinct, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -40,7 +40,7 @@ from ..schemas.broadcasts import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/broadcasts', tags=['Cabinet Admin Broadcasts']) router = APIRouter(prefix='/admin/broadcasts', tags=['Cabinet Admin Broadcasts'])
@@ -255,7 +255,7 @@ async def get_filters(
try: try:
count = await get_target_users_count(db, key) count = await get_target_users_count(db, key)
except Exception as e: except Exception as e:
logger.warning('Failed to get count for filter', key=key, error=e) logger.warning(f'Failed to get count for filter {key}: {e}')
count = 0 count = 0
filters.append( filters.append(
BroadcastFilter( BroadcastFilter(
@@ -272,7 +272,7 @@ async def get_filters(
try: try:
count = await get_target_users_count(db, key) count = await get_target_users_count(db, key)
except Exception as e: except Exception as e:
logger.warning('Failed to get count for custom filter', key=key, error=e) logger.warning(f'Failed to get count for custom filter {key}: {e}')
count = 0 count = 0
custom_filters.append( custom_filters.append(
BroadcastFilter( BroadcastFilter(
@@ -367,7 +367,7 @@ async def preview_broadcast(
try: try:
count = await get_target_users_count(db, request.target) count = await get_target_users_count(db, request.target)
except Exception as e: except Exception as e:
logger.error('Failed to get count for target', target=request.target, error=e) logger.error(f'Failed to get count for target {request.target}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to count recipients', detail='Failed to count recipients',
@@ -450,9 +450,7 @@ async def create_broadcast(
await broadcast_service.start_broadcast(broadcast.id, config) await broadcast_service.start_broadcast(broadcast.id, config)
await db.refresh(broadcast) await db.refresh(broadcast)
logger.info( logger.info(f"Admin {admin.id} created broadcast {broadcast.id} for target '{request.target}'")
'Admin created broadcast for target', admin_id=admin.id, broadcast_id=broadcast.id, target=request.target
)
return _serialize_broadcast(broadcast) return _serialize_broadcast(broadcast)
@@ -496,7 +494,7 @@ async def get_email_filters(
try: try:
count = await _get_email_filter_count(db, key) count = await _get_email_filter_count(db, key)
except Exception as e: except Exception as e:
logger.warning('Failed to get count for email filter', key=key, error=e) logger.warning(f'Failed to get count for email filter {key}: {e}')
count = 0 count = 0
filters.append( filters.append(
@@ -534,7 +532,7 @@ async def preview_email_broadcast(
try: try:
count = await _get_email_filter_count(db, request.target) count = await _get_email_filter_count(db, request.target)
except Exception as e: except Exception as e:
logger.error('Failed to get email count for target', target=request.target, error=e) logger.error(f'Failed to get email count for target {request.target}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to count email recipients', detail='Failed to count email recipients',
@@ -663,13 +661,7 @@ async def create_combined_broadcast(
await db.refresh(broadcast) await db.refresh(broadcast)
logger.info( logger.info(f"Admin {admin.id} created {request.channel} broadcast {broadcast.id} for target '{request.target}'")
'Admin created broadcast for target',
admin_id=admin.id,
channel=request.channel,
broadcast_id=broadcast.id,
target=request.target,
)
return _serialize_broadcast(broadcast) return _serialize_broadcast(broadcast)
@@ -729,6 +721,6 @@ async def stop_broadcast(
await db.commit() await db.commit()
await db.refresh(broadcast) await db.refresh(broadcast)
logger.info('Admin stopped broadcast', admin_id=admin.id, broadcast_id=broadcast_id) logger.info(f'Admin {admin.id} stopped broadcast {broadcast_id}')
return _serialize_broadcast(broadcast) return _serialize_broadcast(broadcast)
+4 -6
View File
@@ -1,8 +1,8 @@
"""Admin routes for per-section cabinet button style configuration.""" """Admin routes for per-section cabinet button style configuration."""
import json import json
import logging
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from app.utils.button_styles_cache import (
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/button-styles', tags=['Admin Button Styles']) router = APIRouter(prefix='/admin/button-styles', tags=['Admin Button Styles'])
@@ -234,9 +234,7 @@ async def update_button_styles(
# Refresh in-process cache # Refresh in-process cache
await load_button_styles_cache() await load_button_styles_cache()
logger.info( logger.info('Admin %s updated button styles for sections: %s', admin.telegram_id, changed_sections)
'Admin updated button styles for sections', telegram_id=admin.telegram_id, changed_sections=changed_sections
)
return _build_response(current) return _build_response(current)
@@ -250,6 +248,6 @@ async def reset_button_styles(
await _set_setting_value(db, BUTTON_STYLES_KEY, json.dumps(DEFAULT_BUTTON_STYLES)) await _set_setting_value(db, BUTTON_STYLES_KEY, json.dumps(DEFAULT_BUTTON_STYLES))
await load_button_styles_cache() await load_button_styles_cache()
logger.info('Admin reset button styles to defaults', telegram_id=admin.telegram_id) logger.info('Admin %s reset button styles to defaults', admin.telegram_id)
return _build_response(DEFAULT_BUTTON_STYLES) return _build_response(DEFAULT_BUTTON_STYLES)
+7 -6
View File
@@ -1,6 +1,7 @@
"""Admin routes for managing advertising campaigns in cabinet.""" """Admin routes for managing advertising campaigns in cabinet."""
import structlog import logging
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -44,7 +45,7 @@ from ..schemas.campaigns import (
from ..schemas.tariffs import TariffListItem from ..schemas.tariffs import TariffListItem
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/campaigns', tags=['Cabinet Admin Campaigns']) router = APIRouter(prefix='/admin/campaigns', tags=['Cabinet Admin Campaigns'])
@@ -377,7 +378,7 @@ async def create_new_campaign(
# Reload to get tariff relationship # Reload to get tariff relationship
campaign = await get_campaign_by_id(db, campaign.id) campaign = await get_campaign_by_id(db, campaign.id)
logger.info('Admin created campaign', admin_id=admin.id, campaign_id=campaign.id, campaign_name=campaign.name) logger.info(f'Admin {admin.id} created campaign {campaign.id}: {campaign.name}')
return await get_campaign(campaign.id, admin, db) return await get_campaign(campaign.id, admin, db)
@@ -446,7 +447,7 @@ async def update_existing_campaign(
if updates: if updates:
await update_campaign(db, campaign, **updates) await update_campaign(db, campaign, **updates)
logger.info('Admin updated campaign', admin_id=admin.id, campaign_id=campaign_id) logger.info(f'Admin {admin.id} updated campaign {campaign_id}')
return await get_campaign(campaign_id, admin, db) return await get_campaign(campaign_id, admin, db)
@@ -474,7 +475,7 @@ async def delete_existing_campaign(
) )
await delete_campaign(db, campaign) await delete_campaign(db, campaign)
logger.info('Admin deleted campaign', admin_id=admin.id, campaign_id=campaign_id, campaign_name=campaign.name) logger.info(f'Admin {admin.id} deleted campaign {campaign_id}: {campaign.name}')
return {'message': 'Campaign deleted successfully'} return {'message': 'Campaign deleted successfully'}
@@ -497,7 +498,7 @@ async def toggle_campaign(
await update_campaign(db, campaign, is_active=new_status) await update_campaign(db, campaign, is_active=new_status)
status_text = 'activated' if new_status else 'deactivated' status_text = 'activated' if new_status else 'deactivated'
logger.info('Admin campaign', admin_id=admin.id, status_text=status_text, campaign_id=campaign_id) logger.info(f'Admin {admin.id} {status_text} campaign {campaign_id}')
return CampaignToggleResponse( return CampaignToggleResponse(
id=campaign_id, id=campaign_id,
+16 -13
View File
@@ -1,9 +1,9 @@
"""Admin routes for managing email notification templates.""" """Admin routes for managing email notification templates."""
import asyncio import asyncio
import logging
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from ..services.email_template_overrides import (
from ..services.email_templates import EmailNotificationTemplates from ..services.email_templates import EmailNotificationTemplates
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/email-templates', tags=['Admin Email Templates']) router = APIRouter(prefix='/admin/email-templates', tags=['Admin Email Templates'])
@@ -505,7 +505,10 @@ async def update_template(
) )
logger.info( logger.info(
'Админ обновил email шаблон /', admin_id=admin.id, notification_type=notification_type, language=language 'Админ %s обновил email шаблон %s/%s',
admin.id,
notification_type,
language,
) )
return {'status': 'ok', 'template': result} return {'status': 'ok', 'template': result}
@@ -530,10 +533,10 @@ async def reset_template(
if deleted: if deleted:
logger.info( logger.info(
'Админ сбросил email шаблон / к дефолту', 'Админ %s сбросил email шаблон %s/%s к дефолту',
admin_id=admin.id, admin.id,
notification_type=notification_type, notification_type,
language=language, language,
) )
return {'status': 'ok', 'was_custom': deleted} return {'status': 'ok', 'was_custom': deleted}
@@ -654,7 +657,7 @@ async def send_test_email(
body_html=body_html, body_html=body_html,
) )
except Exception as e: except Exception as e:
logger.error('Ошибка отправки тестового email', e=e) logger.error('Ошибка отправки тестового email: %s', e)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Failed to send test email: {e!s}', detail=f'Failed to send test email: {e!s}',
@@ -667,11 +670,11 @@ async def send_test_email(
) )
logger.info( logger.info(
'Админ отправил тестовый email / на', 'Админ %s отправил тестовый email %s/%s на %s',
admin_id=admin.id, admin.id,
notification_type=notification_type, notification_type,
language=language, language,
to_email=to_email, to_email,
) )
return {'status': 'ok', 'sent_to': to_email} return {'status': 'ok', 'sent_to': to_email}
+4 -4
View File
@@ -1,8 +1,8 @@
"""Admin routes for payment method configuration in cabinet.""" """Admin routes for payment method configuration in cabinet."""
import logging
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from app.services.payment_method_config_service import (
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/payment-methods', tags=['Cabinet Admin Payment Methods']) router = APIRouter(prefix='/admin/payment-methods', tags=['Cabinet Admin Payment Methods'])
@@ -168,7 +168,7 @@ async def update_payment_methods_order(
): ):
"""Batch update sort order for payment methods.""" """Batch update sort order for payment methods."""
await update_sort_order(db, request.method_ids) await update_sort_order(db, request.method_ids)
logger.info('Admin updated payment methods order', admin_id=admin.id, method_ids=request.method_ids) logger.info(f'Admin {admin.id} updated payment methods order: {request.method_ids}')
return {'success': True} return {'success': True}
@@ -222,7 +222,7 @@ async def update_payment_method(
detail=f'Payment method not found: {method_id}', detail=f'Payment method not found: {method_id}',
) )
logger.info('Admin updated payment method config', admin_id=admin.id, method_id=method_id) logger.info(f'Admin {admin.id} updated payment method config: {method_id}')
defaults = _get_method_defaults() defaults = _get_method_defaults()
return _enrich_config(config, defaults) return _enrich_config(config, defaults)
+3 -10
View File
@@ -1,9 +1,9 @@
"""Admin routes for payment verification in cabinet.""" """Admin routes for payment verification in cabinet."""
import logging
import math import math
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,7 +22,7 @@ from app.services.payment_verification_service import (
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/payments', tags=['Cabinet Admin Payments']) router = APIRouter(prefix='/admin/payments', tags=['Cabinet Admin Payments'])
@@ -406,14 +406,7 @@ async def check_payment_status(
if status_changed: if status_changed:
_, new_status_text = _get_status_info(updated) _, new_status_text = _get_status_info(updated)
message = f'Статус обновлён: {new_status_text}' message = f'Статус обновлён: {new_status_text}'
logger.info( logger.info(f'Admin {admin.id} checked payment {method}/{payment_id}: {old_status} -> {updated.status}')
'Admin checked payment /',
admin_id=admin.id,
method=method,
payment_id=payment_id,
old_status=old_status,
status=updated.status,
)
else: else:
message = 'Статус не изменился' message = 'Статус не изменился'
+9 -24
View File
@@ -1,9 +1,9 @@
"""Admin routes for pinned messages in cabinet.""" """Admin routes for pinned messages in cabinet."""
import logging
import time import time
from datetime import datetime from datetime import datetime
import structlog
from aiogram import Bot from aiogram import Bot
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
@@ -34,7 +34,7 @@ from ..schemas.pinned_messages import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/pinned-messages', tags=['Cabinet Admin Pinned Messages']) router = APIRouter(prefix='/admin/pinned-messages', tags=['Cabinet Admin Pinned Messages'])
@@ -186,9 +186,7 @@ async def create_pinned_message(
if payload.broadcast: if payload.broadcast:
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg) sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
logger.info( logger.info(f'Admin {admin.id} created pinned message #{msg.id} (broadcast={payload.broadcast})')
'Admin created pinned message # (broadcast=)', admin_id=admin.id, message_id=msg.id, broadcast=payload.broadcast
)
return PinnedMessageBroadcastResponse( return PinnedMessageBroadcastResponse(
message=_serialize_pinned_message(msg), message=_serialize_pinned_message(msg),
@@ -231,7 +229,7 @@ async def update_pinned_message(
await db.commit() await db.commit()
await db.refresh(msg) await db.refresh(msg)
logger.info('Admin updated pinned message #', admin_id=admin.id, message_id=message_id) logger.info(f'Admin {admin.id} updated pinned message #{message_id}')
return _serialize_pinned_message(msg) return _serialize_pinned_message(msg)
@@ -275,7 +273,7 @@ async def deactivate_active_message(
if not msg: if not msg:
return None return None
logger.info('Admin deactivated pinned message #', admin_id=admin.id, message_id=msg.id) logger.info(f'Admin {admin.id} deactivated pinned message #{msg.id}')
return _serialize_pinned_message(msg) return _serialize_pinned_message(msg)
@@ -290,12 +288,7 @@ async def unpin_active_message(
unpinned_count, failed_count, was_active = await unpin_active_pinned_message(_get_bot(), db) unpinned_count, failed_count, was_active = await unpin_active_pinned_message(_get_bot(), db)
if was_active: if was_active:
logger.info( logger.info(f'Admin {admin.id} unpinned active message: unpinned={unpinned_count}, failed={failed_count}')
'Admin unpinned active message: unpinned=, failed',
admin_id=admin.id,
unpinned_count=unpinned_count,
failed_count=failed_count,
)
return PinnedMessageUnpinResponse( return PinnedMessageUnpinResponse(
unpinned_count=unpinned_count, unpinned_count=unpinned_count,
@@ -346,9 +339,7 @@ async def activate_pinned_message(
if broadcast: if broadcast:
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg) sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
logger.info( logger.info(f'Admin {admin.id} activated pinned message #{message_id} (broadcast={broadcast})')
'Admin activated pinned message # (broadcast=)', admin_id=admin.id, message_id=message_id, broadcast=broadcast
)
return PinnedMessageBroadcastResponse( return PinnedMessageBroadcastResponse(
message=_serialize_pinned_message(msg), message=_serialize_pinned_message(msg),
@@ -373,13 +364,7 @@ async def broadcast_message(
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg) sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
logger.info( logger.info(f'Admin {admin.id} broadcast pinned message #{message_id}: sent={sent_count}, failed={failed_count}')
'Admin broadcast pinned message #: sent=, failed',
admin_id=admin.id,
message_id=message_id,
sent_count=sent_count,
failed_count=failed_count,
)
return PinnedMessageBroadcastResponse( return PinnedMessageBroadcastResponse(
message=_serialize_pinned_message(msg), message=_serialize_pinned_message(msg),
@@ -409,4 +394,4 @@ async def delete_pinned_message(
await db.delete(msg) await db.delete(msg)
await db.commit() await db.commit()
logger.info('Admin deleted pinned message #', admin_id=admin.id, message_id=message_id) logger.info(f'Admin {admin.id} deleted pinned message #{message_id}')
+13 -5
View File
@@ -3,10 +3,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from aiogram import Bot from aiogram import Bot
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
@@ -37,7 +37,7 @@ from app.utils.miniapp_buttons import build_miniapp_or_callback_button
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/promo-offers', tags=['Admin Promo Offers']) router = APIRouter(prefix='/admin/promo-offers', tags=['Admin Promo Offers'])
@@ -430,7 +430,7 @@ async def _send_promo_notifications(
async def send_single(user: User, offer: DiscountOffer) -> bool: async def send_single(user: User, offer: DiscountOffer) -> bool:
# Skip email-only users (no telegram_id) # Skip email-only users (no telegram_id)
if not user.telegram_id: if not user.telegram_id:
logger.debug('Skipping promo notification for email-only user', user_id=user.id) logger.debug(f'Skipping promo notification for email-only user {user.id}')
return False return False
async with semaphore: async with semaphore:
@@ -459,10 +459,18 @@ async def _send_promo_notifications(
) )
return True return True
except (TelegramForbiddenError, TelegramBadRequest) as exc: except (TelegramForbiddenError, TelegramBadRequest) as exc:
logger.warning('Failed to send promo notification to user', telegram_id=user.telegram_id, exc=exc) logger.warning(
'Failed to send promo notification to user %s: %s',
user.telegram_id,
exc,
)
return False return False
except Exception as exc: except Exception as exc:
logger.error('Error sending promo notification to user', telegram_id=user.telegram_id, exc=exc) logger.error(
'Error sending promo notification to user %s: %s',
user.telegram_id,
exc,
)
return False return False
# Send in batches # Send in batches
+19 -31
View File
@@ -1,9 +1,9 @@
"""Admin routes for RemnaWave management in cabinet.""" """Admin routes for RemnaWave management in cabinet."""
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -76,7 +76,7 @@ except Exception:
remnawave_sync_service = None remnawave_sync_service = None
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/remnawave', tags=['Cabinet Admin RemnaWave']) router = APIRouter(prefix='/admin/remnawave', tags=['Cabinet Admin RemnaWave'])
@@ -380,9 +380,7 @@ async def perform_node_action(
} }
if success: if success:
logger.info( logger.info(f'Admin {admin.telegram_id} performed {payload.action} on node {node_uuid}')
'Admin performed on node', telegram_id=admin.telegram_id, action=payload.action, node_uuid=node_uuid
)
return NodeActionResponse( return NodeActionResponse(
success=True, success=True,
message=messages.get(payload.action, 'Action completed'), message=messages.get(payload.action, 'Action completed'),
@@ -405,7 +403,7 @@ async def restart_all_nodes(
success = await service.restart_all_nodes() success = await service.restart_all_nodes()
if success: if success:
logger.info('Admin restarted all nodes', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} restarted all nodes')
return NodeActionResponse(success=True, message='All nodes restart initiated') return NodeActionResponse(success=True, message='All nodes restart initiated')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -512,9 +510,7 @@ async def create_squad(
squad_uuid = await service.create_squad(payload.name, payload.inbound_uuids) squad_uuid = await service.create_squad(payload.name, payload.inbound_uuids)
if squad_uuid: if squad_uuid:
logger.info( logger.info(f'Admin {admin.telegram_id} created squad {payload.name} ({squad_uuid})')
'Admin created squad', telegram_id=admin.telegram_id, payload_name=payload.name, squad_uuid=squad_uuid
)
return SquadOperationResponse( return SquadOperationResponse(
success=True, success=True,
message='Squad created successfully', message='Squad created successfully',
@@ -549,7 +545,7 @@ async def update_squad(
) )
if success: if success:
logger.info('Admin updated squad', telegram_id=admin.telegram_id, squad_uuid=squad_uuid) logger.info(f'Admin {admin.telegram_id} updated squad {squad_uuid}')
return SquadOperationResponse(success=True, message='Squad updated') return SquadOperationResponse(success=True, message='Squad updated')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -598,7 +594,7 @@ async def perform_squad_action(
message = 'Inbounds updated' if success else 'Failed to update inbounds' message = 'Inbounds updated' if success else 'Failed to update inbounds'
if success: if success:
logger.info('Admin performed on squad', telegram_id=admin.telegram_id, action=action, squad_uuid=squad_uuid) logger.info(f'Admin {admin.telegram_id} performed {action} on squad {squad_uuid}')
return SquadOperationResponse(success=success, message=message) return SquadOperationResponse(success=success, message=message)
@@ -615,7 +611,7 @@ async def delete_squad(
success = await service.delete_squad(squad_uuid) success = await service.delete_squad(squad_uuid)
if success: if success:
logger.info('Admin deleted squad', telegram_id=admin.telegram_id, squad_uuid=squad_uuid) logger.info(f'Admin {admin.telegram_id} deleted squad {squad_uuid}')
return SquadOperationResponse(success=True, message='Squad deleted') return SquadOperationResponse(success=True, message='Squad deleted')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -703,9 +699,7 @@ async def migrate_squad_users(
error=result.get('error'), error=result.get('error'),
) )
logger.info( logger.info(f'Admin {admin.telegram_id} migrated users from {source_uuid} to {target_uuid}')
'Admin migrated users from to', telegram_id=admin.telegram_id, source_uuid=source_uuid, target_uuid=target_uuid
)
return MigrationResponse( return MigrationResponse(
success=True, success=True,
@@ -788,14 +782,14 @@ async def toggle_auto_sync(
if payload.enabled and not current_status.enabled: if payload.enabled and not current_status.enabled:
# Enable - would need to update settings and refresh schedule # Enable - would need to update settings and refresh schedule
remnawave_sync_service.schedule_refresh(run_immediately=True) remnawave_sync_service.schedule_refresh(run_immediately=True)
logger.info('Admin enabled auto sync', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} enabled auto sync')
return SyncResponse( return SyncResponse(
success=True, success=True,
message='Auto sync enabled and scheduled', message='Auto sync enabled and scheduled',
) )
if not payload.enabled and current_status.enabled: if not payload.enabled and current_status.enabled:
# Disable - would need to update settings and stop scheduler # Disable - would need to update settings and stop scheduler
logger.info('Admin disabled auto sync', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} disabled auto sync')
return SyncResponse( return SyncResponse(
success=True, success=True,
message='Auto sync setting change requested. Restart may be required.', message='Auto sync setting change requested. Restart may be required.',
@@ -817,7 +811,7 @@ async def run_auto_sync_now(
detail='Auto sync service is not available', detail='Auto sync service is not available',
) )
logger.info('Admin triggered manual sync', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} triggered manual sync')
result = await remnawave_sync_service.run_sync_now(reason='manual') result = await remnawave_sync_service.run_sync_now(reason='manual')
return AutoSyncRunResponse( return AutoSyncRunResponse(
@@ -845,7 +839,7 @@ async def sync_from_panel(
try: try:
stats = await service.sync_users_from_panel(db, payload.mode) stats = await service.sync_users_from_panel(db, payload.mode)
logger.info('Admin synced from panel (mode: )', telegram_id=admin.telegram_id, mode=payload.mode) logger.info(f'Admin {admin.telegram_id} synced from panel (mode: {payload.mode})')
return SyncResponse( return SyncResponse(
success=True, success=True,
message='Sync from panel completed', message='Sync from panel completed',
@@ -868,7 +862,7 @@ async def sync_to_panel(
_ensure_configured(service) _ensure_configured(service)
stats = await service.sync_users_to_panel(db) stats = await service.sync_users_to_panel(db)
logger.info('Admin synced to panel', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} synced to panel')
return SyncResponse( return SyncResponse(
success=True, success=True,
@@ -898,15 +892,9 @@ async def sync_servers(
try: try:
await cache.delete_pattern('available_countries*') await cache.delete_pattern('available_countries*')
except Exception as e: except Exception as e:
logger.warning('Failed to clear countries cache', error=e) logger.warning(f'Failed to clear countries cache: {e}')
logger.info( logger.info(f'Admin {admin.telegram_id} synced servers: created={created}, updated={updated}, removed={removed}')
'Admin synced servers: created=, updated=, removed',
telegram_id=admin.telegram_id,
created=created,
updated=updated,
removed=removed,
)
return SyncResponse( return SyncResponse(
success=True, success=True,
@@ -930,7 +918,7 @@ async def validate_subscriptions(
_ensure_configured(service) _ensure_configured(service)
stats = await service.validate_and_fix_subscriptions(db) stats = await service.validate_and_fix_subscriptions(db)
logger.info('Admin validated subscriptions', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} validated subscriptions')
return SyncResponse( return SyncResponse(
success=True, success=True,
@@ -949,7 +937,7 @@ async def cleanup_subscriptions(
_ensure_configured(service) _ensure_configured(service)
stats = await service.cleanup_orphaned_subscriptions(db) stats = await service.cleanup_orphaned_subscriptions(db)
logger.info('Admin cleaned up subscriptions', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} cleaned up subscriptions')
return SyncResponse( return SyncResponse(
success=True, success=True,
@@ -968,7 +956,7 @@ async def sync_subscription_statuses(
_ensure_configured(service) _ensure_configured(service)
stats = await service.sync_subscription_statuses(db) stats = await service.sync_subscription_statuses(db)
logger.info('Admin synced subscription statuses', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} synced subscription statuses')
return SyncResponse( return SyncResponse(
success=True, success=True,
+8 -7
View File
@@ -1,6 +1,7 @@
"""Admin routes for managing servers in cabinet.""" """Admin routes for managing servers in cabinet."""
import structlog import logging
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import String, func, select from sqlalchemy import String, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -30,7 +31,7 @@ from ..schemas.servers import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/servers', tags=['Cabinet Admin Servers']) router = APIRouter(prefix='/admin/servers', tags=['Cabinet Admin Servers'])
@@ -183,7 +184,7 @@ async def update_existing_server(
if request.promo_group_ids is not None: if request.promo_group_ids is not None:
await update_server_squad_promo_groups(db, server_id, request.promo_group_ids) await update_server_squad_promo_groups(db, server_id, request.promo_group_ids)
logger.info('Admin updated server', admin_id=admin.id, server_id=server_id) logger.info(f'Admin {admin.id} updated server {server_id}')
return await get_server(server_id, admin, db) return await get_server(server_id, admin, db)
@@ -206,7 +207,7 @@ async def toggle_server(
await update_server_squad(db, server_id, is_available=new_status) await update_server_squad(db, server_id, is_available=new_status)
status_text = 'enabled' if new_status else 'disabled' status_text = 'enabled' if new_status else 'disabled'
logger.info('Admin server', admin_id=admin.id, status_text=status_text, server_id=server_id) logger.info(f'Admin {admin.id} {status_text} server {server_id}')
return ServerToggleResponse( return ServerToggleResponse(
id=server_id, id=server_id,
@@ -233,7 +234,7 @@ async def toggle_server_trial(
await update_server_squad(db, server_id, is_trial_eligible=new_status) await update_server_squad(db, server_id, is_trial_eligible=new_status)
status_text = 'enabled for trial' if new_status else 'disabled for trial' status_text = 'enabled for trial' if new_status else 'disabled for trial'
logger.info('Admin server', admin_id=admin.id, status_text=status_text, server_id=server_id) logger.info(f'Admin {admin.id} {status_text} server {server_id}')
return ServerTrialToggleResponse( return ServerTrialToggleResponse(
id=server_id, id=server_id,
@@ -310,7 +311,7 @@ async def sync_servers(
# Sync with database # Sync with database
created, updated, removed = await sync_with_remnawave(db, squads) created, updated, removed = await sync_with_remnawave(db, squads)
logger.info('Admin synced servers: + ~', admin_id=admin.id, created=created, updated=updated, removed=removed) logger.info(f'Admin {admin.id} synced servers: +{created} ~{updated} -{removed}')
return ServerSyncResponse( return ServerSyncResponse(
created=created, created=created,
@@ -322,7 +323,7 @@ async def sync_servers(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Failed to sync servers', error=e) logger.error(f'Failed to sync servers: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Sync failed: {e!s}', detail=f'Sync failed: {e!s}',
+4 -4
View File
@@ -1,8 +1,8 @@
"""Admin settings routes for cabinet - system configuration management.""" """Admin settings routes for cabinet - system configuration management."""
import logging
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +16,7 @@ from app.services.system_settings_service import (
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/settings', tags=['Admin Settings']) router = APIRouter(prefix='/admin/settings', tags=['Admin Settings'])
@@ -248,7 +248,7 @@ async def update_setting(
raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error
await db.commit() await db.commit()
logger.info('Admin updated setting to', telegram_id=admin.telegram_id, key=key, value=value) logger.info(f'Admin {admin.telegram_id} updated setting {key} to {value}')
return _serialize_definition(definition) return _serialize_definition(definition)
@@ -270,5 +270,5 @@ async def reset_setting(
raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error
await db.commit() await db.commit()
logger.info('Admin reset setting', telegram_id=admin.telegram_id, key=key) logger.info(f'Admin {admin.telegram_id} reset setting {key}')
return _serialize_definition(definition) return _serialize_definition(definition)
+16 -18
View File
@@ -1,10 +1,10 @@
"""Admin routes for statistics dashboard in cabinet.""" """Admin routes for statistics dashboard in cabinet."""
import logging
import sys import sys
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
@@ -29,7 +29,7 @@ from app.services.version_service import version_service
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
_start_time = time.time() _start_time = time.time()
@@ -317,7 +317,7 @@ async def get_dashboard_stats(
) )
except Exception as e: except Exception as e:
logger.error('Failed to get dashboard stats', error=e) logger.error(f'Failed to get dashboard stats: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load dashboard statistics', detail='Failed to load dashboard statistics',
@@ -349,7 +349,7 @@ async def get_system_info(
subscriptions_active=subscriptions_active, subscriptions_active=subscriptions_active,
) )
except Exception as e: except Exception as e:
logger.error('Failed to get system info', error=e) logger.error(f'Failed to get system info: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load system information', detail='Failed to load system information',
@@ -364,7 +364,7 @@ async def get_nodes_status(
try: try:
return await _get_nodes_overview() return await _get_nodes_overview()
except Exception as e: except Exception as e:
logger.error('Failed to get nodes status', error=e) logger.error(f'Failed to get nodes status: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load nodes status', detail='Failed to load nodes status',
@@ -382,7 +382,7 @@ async def restart_node(
success = await service.manage_node(node_uuid, 'restart') success = await service.manage_node(node_uuid, 'restart')
if success: if success:
logger.info('Admin restarted node', admin_id=admin.id, node_uuid=node_uuid) logger.info(f'Admin {admin.id} restarted node {node_uuid}')
return {'success': True, 'message': 'Node restart initiated'} return {'success': True, 'message': 'Node restart initiated'}
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -391,7 +391,7 @@ async def restart_node(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Failed to restart node', node_uuid=node_uuid, error=e) logger.error(f'Failed to restart node {node_uuid}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to restart node', detail='Failed to restart node',
@@ -420,7 +420,7 @@ async def toggle_node(
success = await service.manage_node(node_uuid, action) success = await service.manage_node(node_uuid, action)
if success: if success:
logger.info('Admin d node', admin_id=admin.id, action=action, node_uuid=node_uuid) logger.info(f'Admin {admin.id} {action}d node {node_uuid}')
return {'success': True, 'message': f'Node {action}d', 'is_disabled': not is_disabled} return {'success': True, 'message': f'Node {action}d', 'is_disabled': not is_disabled}
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -429,7 +429,7 @@ async def toggle_node(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Failed to toggle node', node_uuid=node_uuid, error=e) logger.error(f'Failed to toggle node {node_uuid}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to toggle node', detail='Failed to toggle node',
@@ -480,7 +480,7 @@ async def _get_nodes_overview() -> NodesOverview:
nodes=node_statuses, nodes=node_statuses,
) )
except Exception as e: except Exception as e:
logger.warning('Failed to get nodes from RemnaWave', error=e) logger.warning(f'Failed to get nodes from RemnaWave: {e}')
# Return empty data if RemnaWave is unavailable # Return empty data if RemnaWave is unavailable
return NodesOverview( return NodesOverview(
total=0, total=0,
@@ -560,9 +560,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
) )
purchased_month = month_result.scalar() or 0 purchased_month = month_result.scalar() or 0
logger.info( logger.info(f"📊 Тариф '{tariff.name}': активных={active_count}, триал={trial_count}")
'📊 Тариф активных=, триал', tariff_name=tariff.name, active_count=active_count, trial_count=trial_count
)
tariff_items.append( tariff_items.append(
TariffStatItem( TariffStatItem(
@@ -578,7 +576,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
total_tariff_subscriptions += active_count total_tariff_subscriptions += active_count
logger.info('📊 Всего подписок по тарифам', total_tariff_subscriptions=total_tariff_subscriptions) logger.info(f'📊 Всего подписок по тарифам: {total_tariff_subscriptions}')
return TariffStats( return TariffStats(
tariffs=tariff_items, tariffs=tariff_items,
@@ -586,7 +584,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
) )
except Exception as e: except Exception as e:
logger.error('Failed to get tariff stats', error=e, exc_info=True) logger.error(f'Failed to get tariff stats: {e}', exc_info=True)
return None return None
@@ -802,7 +800,7 @@ async def get_top_referrers(
) )
except Exception as e: except Exception as e:
logger.error('Failed to get top referrers', error=e, exc_info=True) logger.error(f'Failed to get top referrers: {e}', exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load referrers statistics', detail='Failed to load referrers statistics',
@@ -859,7 +857,7 @@ async def get_top_campaigns(
) )
except Exception as e: except Exception as e:
logger.error('Failed to get top campaigns', error=e, exc_info=True) logger.error(f'Failed to get top campaigns: {e}', exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load campaigns statistics', detail='Failed to load campaigns statistics',
@@ -998,7 +996,7 @@ async def get_recent_payments(
) )
except Exception as e: except Exception as e:
logger.error('Failed to get recent payments', error=e, exc_info=True) logger.error(f'Failed to get recent payments: {e}', exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load recent payments', detail='Failed to load recent payments',
+9 -14
View File
@@ -1,6 +1,7 @@
"""Admin routes for managing tariffs in cabinet.""" """Admin routes for managing tariffs in cabinet."""
import structlog import logging
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -37,7 +38,7 @@ from ..schemas.tariffs import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/tariffs', tags=['Cabinet Admin Tariffs']) router = APIRouter(prefix='/admin/tariffs', tags=['Cabinet Admin Tariffs'])
@@ -168,7 +169,7 @@ async def update_tariff_order(
await reorder_tariffs(db, request.tariff_ids) await reorder_tariffs(db, request.tariff_ids)
await db.commit() await db.commit()
logger.info('Admin updated tariff order', admin_id=admin.id, tariff_ids=request.tariff_ids) logger.info(f'Admin {admin.id} updated tariff order: {request.tariff_ids}')
return {'message': 'Tariff order updated successfully'} return {'message': 'Tariff order updated successfully'}
@@ -294,7 +295,7 @@ async def create_new_tariff(
traffic_reset_mode=request.traffic_reset_mode, traffic_reset_mode=request.traffic_reset_mode,
) )
logger.info('Admin created tariff', admin_id=admin.id, tariff_id=tariff.id, tariff_name=tariff.name) logger.info(f'Admin {admin.id} created tariff {tariff.id}: {tariff.name}')
# Перезагружаем периоды из БД для синхронизации с ботом # Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db) await load_period_prices_from_db(db)
@@ -389,7 +390,7 @@ async def update_existing_tariff(
if request.promo_group_ids is not None: if request.promo_group_ids is not None:
await set_tariff_promo_groups(db, tariff, request.promo_group_ids) await set_tariff_promo_groups(db, tariff, request.promo_group_ids)
logger.info('Admin updated tariff', admin_id=admin.id, tariff_id=tariff_id) logger.info(f'Admin {admin.id} updated tariff {tariff_id}')
# Перезагружаем периоды из БД для синхронизации с ботом # Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db) await load_period_prices_from_db(db)
@@ -413,13 +414,7 @@ async def delete_existing_tariff(
subs_count = await get_tariff_subscriptions_count(db, tariff_id) subs_count = await get_tariff_subscriptions_count(db, tariff_id)
await delete_tariff(db, tariff) await delete_tariff(db, tariff)
logger.info( logger.info(f'Admin {admin.id} deleted tariff {tariff_id}: {tariff.name} (affected subscriptions: {subs_count})')
'Admin deleted tariff (affected subscriptions: )',
admin_id=admin.id,
tariff_id=tariff_id,
tariff_name=tariff.name,
subs_count=subs_count,
)
# Перезагружаем периоды из БД для синхронизации с ботом # Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db) await load_period_prices_from_db(db)
@@ -445,7 +440,7 @@ async def toggle_tariff(
await update_tariff(db, tariff, is_active=new_status) await update_tariff(db, tariff, is_active=new_status)
status_text = 'activated' if new_status else 'deactivated' status_text = 'activated' if new_status else 'deactivated'
logger.info('Admin tariff', admin_id=admin.id, status_text=status_text, tariff_id=tariff_id) logger.info(f'Admin {admin.id} {status_text} tariff {tariff_id}')
# Перезагружаем периоды из БД для синхронизации с ботом # Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db) await load_period_prices_from_db(db)
@@ -488,7 +483,7 @@ async def toggle_trial_tariff(
await update_tariff(db, tariff, is_trial_available=new_status) await update_tariff(db, tariff, is_trial_available=new_status)
status_text = 'set as trial' if new_status else 'removed from trial' status_text = 'set as trial' if new_status else 'removed from trial'
logger.info('Admin tariff', admin_id=admin.id, status_text=status_text, tariff_id=tariff_id) logger.info(f'Admin {admin.id} {status_text} tariff {tariff_id}')
return TariffTrialResponse( return TariffTrialResponse(
id=tariff_id, id=tariff_id,
+6 -6
View File
@@ -1,9 +1,9 @@
"""Admin tickets routes for cabinet.""" """Admin tickets routes for cabinet."""
import logging
import math import math
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy import desc, func, select from sqlalchemy import desc, func, select
@@ -20,7 +20,7 @@ from ..dependencies import get_cabinet_db, get_current_admin_user
from ..schemas.tickets import TicketMessageResponse from ..schemas.tickets import TicketMessageResponse
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/tickets', tags=['Cabinet Admin Tickets']) router = APIRouter(prefix='/admin/tickets', tags=['Cabinet Admin Tickets'])
@@ -317,7 +317,7 @@ async def update_ticket_settings(
env_file.write_text('\n'.join(new_lines) + '\n') env_file.write_text('\n'.join(new_lines) + '\n')
logger.info('Updated ticket settings in .env file') logger.info('Updated ticket settings in .env file')
except Exception as e: except Exception as e:
logger.warning('Failed to update .env file', error=e) logger.warning(f'Failed to update .env file: {e}')
return TicketSettingsResponse( return TicketSettingsResponse(
sla_enabled=settings.SUPPORT_TICKET_SLA_ENABLED, sla_enabled=settings.SUPPORT_TICKET_SLA_ENABLED,
@@ -473,11 +473,11 @@ async def reply_to_ticket(
await notify_user_about_ticket_reply(bot, ticket, request.message, db) await notify_user_about_ticket_reply(bot, ticket, request.message, db)
except Exception as e: except Exception as e:
logger.warning('Failed to notify user about ticket reply', error=e) logger.warning(f'Failed to notify user about ticket reply: {e}')
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.warning('Failed to send Telegram notification', error=e) logger.warning(f'Failed to send Telegram notification: {e}')
# Уведомить пользователя в кабинете # Уведомить пользователя в кабинете
try: try:
@@ -488,7 +488,7 @@ async def reply_to_ticket(
# Отправить WebSocket уведомление # Отправить WebSocket уведомление
await notify_user_ticket_reply(ticket.user_id, ticket.id, (request.message or '')[:100]) await notify_user_ticket_reply(ticket.user_id, ticket.id, (request.message or '')[:100])
except Exception as e: except Exception as e:
logger.warning('Failed to create cabinet notification for admin reply', error=e) logger.warning(f'Failed to create cabinet notification for admin reply: {e}')
return _message_to_response(message) return _message_to_response(message)
+4 -4
View File
@@ -3,10 +3,10 @@
import asyncio import asyncio
import csv import csv
import io import io
import logging
import time import time
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog
from aiogram import Bot from aiogram import Bot
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
@@ -32,7 +32,7 @@ from ..schemas.traffic import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/traffic', tags=['Admin Traffic']) router = APIRouter(prefix='/admin/traffic', tags=['Admin Traffic'])
@@ -110,7 +110,7 @@ async def _aggregate_traffic(
stats = await api.get_bandwidth_stats_node_users_legacy(node.uuid, start_str, end_str) stats = await api.get_bandwidth_stats_node_users_legacy(node.uuid, start_str, end_str)
return node.uuid, stats return node.uuid, stats
except Exception: except Exception:
logger.warning('Failed to get traffic for node', node_name=node.name, exc_info=True) logger.warning('Failed to get traffic for node %s', node.name, exc_info=True)
return node.uuid, None return node.uuid, None
results = await asyncio.gather(*(fetch_node_users(n) for n in nodes)) results = await asyncio.gather(*(fetch_node_users(n) for n in nodes))
@@ -685,7 +685,7 @@ async def export_traffic_csv(
caption=f'Traffic usage report ({period_label})\nUsers: {len(rows)}', caption=f'Traffic usage report ({period_label})\nUsers: {len(rows)}',
) )
except Exception: except Exception:
logger.error('Failed to send CSV to admin', telegram_id=admin.telegram_id, exc_info=True) logger.error('Failed to send CSV to admin %s', admin.telegram_id, exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to send CSV report. Please try again later.', detail='Failed to send CSV report. Please try again later.',
+5 -5
View File
@@ -1,9 +1,9 @@
"""Admin routes for version and release information.""" """Admin routes for version and release information."""
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import aiohttp import aiohttp
import structlog
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel from pydantic import BaseModel
@@ -13,7 +13,7 @@ from app.services.version_service import version_service
from ..dependencies import get_current_admin_user from ..dependencies import get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/updates', tags=['Cabinet Admin Updates']) router = APIRouter(prefix='/admin/updates', tags=['Cabinet Admin Updates'])
@@ -76,15 +76,15 @@ async def _fetch_cabinet_releases(force: bool = False) -> list[dict]:
) )
_cabinet_cache['releases'] = releases _cabinet_cache['releases'] = releases
_cabinet_last_check = datetime.now() _cabinet_last_check = datetime.now()
logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases)) logger.info('Fetched %d cabinet releases from GitHub', len(releases))
return releases return releases
logger.warning('GitHub API returned status for cabinet releases', response_status=response.status) logger.warning('GitHub API returned status %d for cabinet releases', response.status)
return _cabinet_cache.get('releases', []) return _cabinet_cache.get('releases', [])
except TimeoutError: except TimeoutError:
logger.warning('Timeout fetching cabinet releases from GitHub') logger.warning('Timeout fetching cabinet releases from GitHub')
return _cabinet_cache.get('releases', []) return _cabinet_cache.get('releases', [])
except Exception as e: except Exception as e:
logger.error('Error fetching cabinet releases', e=e) logger.error('Error fetching cabinet releases: %s', e)
return _cabinet_cache.get('releases', []) return _cabinet_cache.get('releases', [])
+51 -76
View File
@@ -1,8 +1,8 @@
"""Admin routes for managing users in cabinet.""" """Admin routes for managing users in cabinet."""
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import Integer, and_, func, or_, select from sqlalchemy import Integer, and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -91,7 +91,7 @@ from ..schemas.users import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/users', tags=['Cabinet Admin Users']) router = APIRouter(prefix='/admin/users', tags=['Cabinet Admin Users'])
@@ -219,7 +219,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
service = RemnaWaveService() service = RemnaWaveService()
if not service.is_configured: if not service.is_configured:
logger.warning('Remnawave not configured, skipping panel sync for user', user_id=user.id) logger.warning(f'Remnawave not configured, skipping panel sync for user {user.id}')
return {'skipped': True, 'reason': 'Remnawave not configured'} return {'skipped': True, 'reason': 'Remnawave not configured'}
is_active = ( is_active = (
@@ -260,7 +260,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
if panel_uuid: if panel_uuid:
existing_user = await api.get_user_by_uuid(panel_uuid) existing_user = await api.get_user_by_uuid(panel_uuid)
if not existing_user: if not existing_user:
logger.warning('User has stale remnawave_uuid clearing', user_id=user.id, panel_uuid=panel_uuid) logger.warning(f'User {user.id} has stale remnawave_uuid {panel_uuid}, clearing')
panel_uuid = None panel_uuid = None
user.remnawave_uuid = None user.remnawave_uuid = None
@@ -299,7 +299,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
try: try:
await api.update_user(**update_kwargs) await api.update_user(**update_kwargs)
changes['action'] = 'updated' changes['action'] = 'updated'
logger.info('Updated user in Remnawave panel', user_id=user.id) logger.info(f'Updated user {user.id} in Remnawave panel')
except Exception as update_error: except Exception as update_error:
if hasattr(update_error, 'status_code') and update_error.status_code == 404: if hasattr(update_error, 'status_code') and update_error.status_code == 404:
panel_uuid = None # Will create new panel_uuid = None # Will create new
@@ -328,7 +328,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
subscription.subscription_url = new_panel_user.subscription_url subscription.subscription_url = new_panel_user.subscription_url
changes['action'] = 'created' changes['action'] = 'created'
changes['panel_uuid'] = new_panel_user.uuid changes['panel_uuid'] = new_panel_user.uuid
logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid) logger.info(f'Created user {user.id} in Remnawave panel: {new_panel_user.uuid}')
user.last_remnawave_sync = datetime.utcnow() user.last_remnawave_sync = datetime.utcnow()
await db.commit() await db.commit()
@@ -336,7 +336,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
return changes return changes
except Exception as e: except Exception as e:
logger.error('Error syncing user to panel', user_id=user.id, error=e) logger.error(f'Error syncing user {user.id} to panel: {e}')
return {'error': str(e)} return {'error': str(e)}
@@ -709,7 +709,7 @@ async def get_user_panel_info(
last_node_name = node.node_name last_node_name = node.node_name
break break
except Exception: except Exception:
logger.warning('Failed to resolve node name for user', user_id=user_id) logger.warning(f'Failed to resolve node name for user {user_id}')
return UserPanelInfoResponse( return UserPanelInfoResponse(
found=True, found=True,
@@ -728,7 +728,7 @@ async def get_user_panel_info(
) )
except Exception as e: except Exception as e:
logger.error('Error getting panel info for user', user_id=user_id, error=e) logger.error(f'Error getting panel info for user {user_id}: {e}')
return UserPanelInfoResponse(found=False) return UserPanelInfoResponse(found=False)
@@ -812,7 +812,7 @@ async def get_user_node_usage(
return UserNodeUsageResponse(items=items, categories=categories) return UserNodeUsageResponse(items=items, categories=categories)
except Exception as e: except Exception as e:
logger.error('Error getting node usage for user', user_id=user_id, error=e) logger.error(f'Error getting node usage for user {user_id}: {e}')
return UserNodeUsageResponse(items=[]) return UserNodeUsageResponse(items=[])
@@ -877,12 +877,8 @@ async def update_user_balance(
await db.refresh(user) await db.refresh(user)
logger.info( logger.info(
'Admin updated balance for user', f'Admin {admin.id} updated balance for user {user_id}: '
admin_id=admin.id, f'{old_balance} -> {user.balance_kopeks} ({request.amount_kopeks:+d})'
user_id=user_id,
old_balance=old_balance,
balance_kopeks=user.balance_kopeks,
amount_kopeks=format(request.amount_kopeks, '+d'),
) )
return UpdateBalanceResponse( return UpdateBalanceResponse(
@@ -966,7 +962,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, new_sub) await _sync_subscription_to_panel(db, user, new_sub)
logger.info('Admin created subscription for user', admin_id=admin.id, user_id=user_id) logger.info(f'Admin {admin.id} created subscription for user {user_id}')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -993,9 +989,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info( logger.info(f'Admin {admin.id} extended subscription for user {user_id} by {request.days} days')
'Admin extended subscription for user by days', admin_id=admin.id, user_id=user_id, days=request.days
)
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1022,7 +1016,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info('Admin set end_date for user subscription', admin_id=admin.id, user_id=user_id) logger.info(f'Admin {admin.id} set end_date for user {user_id} subscription')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1056,7 +1050,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info('Admin changed tariff for user to', admin_id=admin.id, user_id=user_id, tariff_name=tariff.name) logger.info(f'Admin {admin.id} changed tariff for user {user_id} to {tariff.name}')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1077,7 +1071,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info('Admin updated traffic for user', admin_id=admin.id, user_id=user_id) logger.info(f'Admin {admin.id} updated traffic for user {user_id}')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1097,7 +1091,7 @@ async def update_user_subscription(
await db.refresh(subscription) await db.refresh(subscription)
state = 'enabled' if request.autopay_enabled else 'disabled' state = 'enabled' if request.autopay_enabled else 'disabled'
logger.info('Admin autopay for user', admin_id=admin.id, state=state, user_id=user_id) logger.info(f'Admin {admin.id} {state} autopay for user {user_id}')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1114,7 +1108,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info('Admin cancelled subscription for user', admin_id=admin.id, user_id=user_id) logger.info(f'Admin {admin.id} cancelled subscription for user {user_id}')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1133,7 +1127,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info('Admin activated subscription for user', admin_id=admin.id, user_id=user_id) logger.info(f'Admin {admin.id} activated subscription for user {user_id}')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1157,7 +1151,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info('Admin added traffic for user', admin_id=admin.id, traffic_gb=request.traffic_gb, user_id=user_id) logger.info(f'Admin {admin.id} added {request.traffic_gb} GB traffic for user {user_id}')
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1217,11 +1211,7 @@ async def update_user_subscription(
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info( logger.info(
'Admin removed traffic purchase ( GB) for user', f'Admin {admin.id} removed traffic purchase {request.traffic_purchase_id} ({removed_gb} GB) for user {user_id}'
admin_id=admin.id,
traffic_purchase_id=request.traffic_purchase_id,
removed_gb=removed_gb,
user_id=user_id,
) )
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
@@ -1244,9 +1234,7 @@ async def update_user_subscription(
# Sync to Remnawave panel # Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription) await _sync_subscription_to_panel(db, user, subscription)
logger.info( logger.info(f'Admin {admin.id} set device limit to {request.device_limit} for user {user_id}')
'Admin set device limit to for user', admin_id=admin.id, device_limit=request.device_limit, user_id=user_id
)
return UpdateSubscriptionResponse( return UpdateSubscriptionResponse(
success=True, success=True,
@@ -1396,7 +1384,7 @@ async def update_user_status(
if request.reason: if request.reason:
action += f' (reason: {request.reason})' action += f' (reason: {request.reason})'
logger.info('Admin changed status for user', admin_id=admin.id, user_id=user_id, action=action) logger.info(f'Admin {admin.id} changed status for user {user_id}: {action}')
return UpdateUserStatusResponse( return UpdateUserStatusResponse(
success=True, success=True,
@@ -1461,11 +1449,8 @@ async def update_user_restrictions(
await db.refresh(user) await db.refresh(user)
logger.info( logger.info(
'Admin updated restrictions for user topup=, subscription', f'Admin {admin.id} updated restrictions for user {user_id}: '
admin_id=admin.id, f'topup={user.restriction_topup}, subscription={user.restriction_subscription}'
user_id=user_id,
restriction_topup=user.restriction_topup,
restriction_subscription=user.restriction_subscription,
) )
return UpdateRestrictionsResponse( return UpdateRestrictionsResponse(
@@ -1516,11 +1501,7 @@ async def update_user_promo_group(
await db.refresh(user) await db.refresh(user)
logger.info( logger.info(
'Admin changed promo group for user', f'Admin {admin.id} changed promo group for user {user_id}: {old_promo_group_id} -> {new_promo_group_id}'
admin_id=admin.id,
user_id=user_id,
old_promo_group_id=old_promo_group_id,
new_promo_group_id=new_promo_group_id,
) )
return UpdatePromoGroupResponse( return UpdatePromoGroupResponse(
@@ -1556,11 +1537,7 @@ async def update_user_referral_commission(
await db.commit() await db.commit()
logger.info( logger.info(
'Admin changed referral commission for user', f'Admin {admin.id} changed referral commission for user {user_id}: {old_commission} -> {request.commission_percent}'
admin_id=admin.id,
user_id=user_id,
old_commission=old_commission,
commission_percent=request.commission_percent,
) )
return UpdateReferralCommissionResponse( return UpdateReferralCommissionResponse(
@@ -1623,7 +1600,7 @@ async def get_user_devices(
) )
except Exception as e: except Exception as e:
logger.error('Error fetching devices for user', user_id=user_id, error=e) logger.error(f'Error fetching devices for user {user_id}: {e}')
return UserDevicesResponse() return UserDevicesResponse()
@@ -1650,12 +1627,12 @@ async def delete_user_device(
success = await api.remove_device(user.remnawave_uuid, hwid) success = await api.remove_device(user.remnawave_uuid, hwid)
if success: if success:
logger.info('Admin deleted device for user', admin_id=admin.id, hwid=hwid, user_id=user_id) logger.info(f'Admin {admin.id} deleted device {hwid} for user {user_id}')
return DeleteDeviceResponse(success=True, message='Device deleted', deleted_hwid=hwid) return DeleteDeviceResponse(success=True, message='Device deleted', deleted_hwid=hwid)
return DeleteDeviceResponse(success=False, message='Failed to delete device') return DeleteDeviceResponse(success=False, message='Failed to delete device')
except Exception as e: except Exception as e:
logger.error('Error deleting device for user', hwid=hwid, user_id=user_id, error=e) logger.error(f'Error deleting device {hwid} for user {user_id}: {e}')
return DeleteDeviceResponse(success=False, message=str(e)) return DeleteDeviceResponse(success=False, message=str(e))
@@ -1695,11 +1672,11 @@ async def reset_user_devices(
except Exception: except Exception:
pass pass
logger.info('Admin reset devices for user /', admin_id=admin.id, user_id=user_id, deleted=deleted, total=total) logger.info(f'Admin {admin.id} reset devices for user {user_id}: {deleted}/{total}')
return ResetDevicesResponse(success=True, message=f'Deleted {deleted}/{total} devices', deleted_count=deleted) return ResetDevicesResponse(success=True, message=f'Deleted {deleted}/{total} devices', deleted_count=deleted)
except Exception as e: except Exception as e:
logger.error('Error resetting devices for user', user_id=user_id, error=e) logger.error(f'Error resetting devices for user {user_id}: {e}')
return ResetDevicesResponse(success=False, message=str(e)) return ResetDevicesResponse(success=False, message=str(e))
@@ -1736,7 +1713,7 @@ async def delete_user(
action = 'permanently deleted' action = 'permanently deleted'
reason_text = f' (reason: {request.reason})' if request.reason else '' reason_text = f' (reason: {request.reason})' if request.reason else ''
logger.info('Admin user', admin_id=admin.id, action=action, user_id=user_id, reason_text=reason_text) logger.info(f'Admin {admin.id} {action} user {user_id}{reason_text}')
return DeleteUserResponse( return DeleteUserResponse(
success=True, success=True,
@@ -1779,7 +1756,7 @@ async def full_delete_user(
deleted_from_panel = request.delete_from_panel and user.remnawave_uuid is not None deleted_from_panel = request.delete_from_panel and user.remnawave_uuid is not None
reason_text = f' (reason: {request.reason})' if request.reason else '' reason_text = f' (reason: {request.reason})' if request.reason else ''
logger.info('Admin fully deleted user', admin_id=admin.id, user_id=user_id, reason_text=reason_text) logger.info(f'Admin {admin.id} fully deleted user {user_id}{reason_text}')
return FullDeleteUserResponse( return FullDeleteUserResponse(
success=success, success=success,
@@ -1823,9 +1800,9 @@ async def reset_user_trial(
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
await subscription_service.disable_remnawave_user(user.remnawave_uuid) await subscription_service.disable_remnawave_user(user.remnawave_uuid)
logger.info('Disabled Remnawave user for trial reset', remnawave_uuid=user.remnawave_uuid) logger.info(f'Disabled Remnawave user {user.remnawave_uuid} for trial reset')
except Exception as e: except Exception as e:
logger.warning('Failed to disable Remnawave user during trial reset', error=e) logger.warning(f'Failed to disable Remnawave user during trial reset: {e}')
# Delete subscription from database # Delete subscription from database
from sqlalchemy import delete from sqlalchemy import delete
@@ -1842,7 +1819,7 @@ async def reset_user_trial(
await db.commit() await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else '' reason_text = f' (reason: {request.reason})' if request.reason else ''
logger.info('Admin reset trial for user', admin_id=admin.id, user_id=user_id, reason_text=reason_text) logger.info(f'Admin {admin.id} reset trial for user {user_id}{reason_text}')
return ResetTrialResponse( return ResetTrialResponse(
success=True, success=True,
@@ -1894,10 +1871,10 @@ async def reset_user_subscription(
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid) panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid)
if panel_deactivated: if panel_deactivated:
logger.info('Disabled Remnawave user for subscription reset', remnawave_uuid=user.remnawave_uuid) logger.info(f'Disabled Remnawave user {user.remnawave_uuid} for subscription reset')
except Exception as e: except Exception as e:
panel_error = str(e) panel_error = str(e)
logger.warning('Failed to disable Remnawave user during subscription reset', error=e) logger.warning(f'Failed to disable Remnawave user during subscription reset: {e}')
# Delete subscription from database # Delete subscription from database
from sqlalchemy import delete from sqlalchemy import delete
@@ -1911,7 +1888,7 @@ async def reset_user_subscription(
await db.commit() await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else '' reason_text = f' (reason: {request.reason})' if request.reason else ''
logger.info('Admin reset subscription for user', admin_id=admin.id, user_id=user_id, reason_text=reason_text) logger.info(f'Admin {admin.id} reset subscription for user {user_id}{reason_text}')
return ResetSubscriptionResponse( return ResetSubscriptionResponse(
success=True, success=True,
@@ -1956,10 +1933,10 @@ async def disable_user(
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid) panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid)
if panel_deactivated: if panel_deactivated:
logger.info('Disabled Remnawave user', remnawave_uuid=user.remnawave_uuid) logger.info(f'Disabled Remnawave user {user.remnawave_uuid}')
except Exception as e: except Exception as e:
panel_error = str(e) panel_error = str(e)
logger.warning('Failed to disable Remnawave user', error=e) logger.warning(f'Failed to disable Remnawave user: {e}')
# Deactivate subscription in bot database # Deactivate subscription in bot database
if user.subscription: if user.subscription:
@@ -1967,7 +1944,7 @@ async def disable_user(
await deactivate_subscription(db, user.subscription) await deactivate_subscription(db, user.subscription)
subscription_deactivated = True subscription_deactivated = True
logger.info('Deactivated subscription for user', user_id=user_id) logger.info(f'Deactivated subscription for user {user_id}')
# Block user account # Block user account
user.status = UserStatus.BLOCKED.value user.status = UserStatus.BLOCKED.value
@@ -1975,7 +1952,7 @@ async def disable_user(
await db.commit() await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else '' reason_text = f' (reason: {request.reason})' if request.reason else ''
logger.info('Admin disabled user', admin_id=admin.id, user_id=user_id, reason_text=reason_text) logger.info(f'Admin {admin.id} disabled user {user_id}{reason_text}')
return DisableUserResponse( return DisableUserResponse(
success=True, success=True,
@@ -2222,7 +2199,7 @@ async def get_user_sync_status(
differences.append(f'Squads mismatch ({", ".join(squad_diff_parts)})') differences.append(f'Squads mismatch ({", ".join(squad_diff_parts)})')
except Exception as e: except Exception as e:
logger.warning('Failed to get panel data for user', user_id=user_id, error=e) logger.warning(f'Failed to get panel data for user {user_id}: {e}')
differences.append(f'Error fetching panel data: {e!s}') differences.append(f'Error fetching panel data: {e!s}')
return PanelSyncStatusResponse( return PanelSyncStatusResponse(
@@ -2432,9 +2409,7 @@ async def sync_user_from_panel(
await db.commit() await db.commit()
logger.info( logger.info(f'Admin {admin.id} synced user {user_id} from panel. Changes: {list(changes.keys())}')
'Admin synced user from panel. Changes', admin_id=admin.id, user_id=user_id, value=list(changes.keys())
)
return SyncFromPanelResponse( return SyncFromPanelResponse(
success=True, success=True,
@@ -2447,7 +2422,7 @@ async def sync_user_from_panel(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Error syncing user from panel', user_id=user_id, error=e) logger.error(f'Error syncing user {user_id} from panel: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Sync error: {e!s}', detail=f'Sync error: {e!s}',
@@ -2535,7 +2510,7 @@ async def sync_user_to_panel(
if panel_uuid: if panel_uuid:
existing_user = await api.get_user_by_uuid(panel_uuid) existing_user = await api.get_user_by_uuid(panel_uuid)
if not existing_user: if not existing_user:
logger.warning('User has stale remnawave_uuid clearing', user_id=user.id, panel_uuid=panel_uuid) logger.warning(f'User {user.id} has stale remnawave_uuid {panel_uuid}, clearing')
panel_uuid = None panel_uuid = None
user.remnawave_uuid = None user.remnawave_uuid = None
@@ -2625,7 +2600,7 @@ async def sync_user_to_panel(
await db.commit() await db.commit()
logger.info('Admin synced user to panel. Action', admin_id=admin.id, user_id=user_id, action=action) logger.info(f'Admin {admin.id} synced user {user_id} to panel. Action: {action}')
return SyncToPanelResponse( return SyncToPanelResponse(
success=True, success=True,
@@ -2639,7 +2614,7 @@ async def sync_user_to_panel(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Error syncing user to panel', user_id=user_id, error=e) logger.error(f'Error syncing user {user_id} to panel: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Sync error: {e!s}', detail=f'Sync error: {e!s}',
+7 -7
View File
@@ -2,10 +2,10 @@
API роуты колеса удачи для администраторов. API роуты колеса удачи для администраторов.
""" """
import logging
import math import math
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -35,7 +35,7 @@ from app.database.models import User
from app.services.wheel_service import wheel_service from app.services.wheel_service import wheel_service
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/wheel', tags=['Admin Fortune Wheel']) router = APIRouter(prefix='/admin/wheel', tags=['Admin Fortune Wheel'])
@@ -107,7 +107,7 @@ async def update_admin_wheel_config(
config = await update_wheel_config(db, **update_data) config = await update_wheel_config(db, **update_data)
logger.info('🎡 Admin updated wheel config', telegram_id=admin.telegram_id, update_data=update_data) logger.info(f'🎡 Admin {admin.telegram_id} updated wheel config: {update_data}')
# Возвращаем полную конфигурацию # Возвращаем полную конфигурацию
prizes = await get_wheel_prizes(db, config.id, active_only=False) prizes = await get_wheel_prizes(db, config.id, active_only=False)
@@ -211,7 +211,7 @@ async def create_prize(
promo_traffic_gb=request.promo_traffic_gb, promo_traffic_gb=request.promo_traffic_gb,
) )
logger.info('🎁 Admin created prize', telegram_id=admin.telegram_id, display_name=prize.display_name) logger.info(f'🎁 Admin {admin.telegram_id} created prize: {prize.display_name}')
return WheelPrizeAdminResponse( return WheelPrizeAdminResponse(
id=prize.id, id=prize.id,
@@ -261,7 +261,7 @@ async def update_prize(
detail='Prize not found', detail='Prize not found',
) )
logger.info('🎁 Admin updated prize', telegram_id=admin.telegram_id, prize_id=prize_id, update_data=update_data) logger.info(f'🎁 Admin {admin.telegram_id} updated prize {prize_id}: {update_data}')
return WheelPrizeAdminResponse( return WheelPrizeAdminResponse(
id=prize.id, id=prize.id,
@@ -298,7 +298,7 @@ async def delete_prize_endpoint(
detail='Prize not found', detail='Prize not found',
) )
logger.info('🗑️ Admin deleted prize', telegram_id=admin.telegram_id, prize_id=prize_id) logger.info(f'🗑️ Admin {admin.telegram_id} deleted prize {prize_id}')
@router.post('/prizes/reorder', status_code=status.HTTP_200_OK) @router.post('/prizes/reorder', status_code=status.HTTP_200_OK)
@@ -309,7 +309,7 @@ async def reorder_prizes(
): ):
"""Переупорядочить призы.""" """Переупорядочить призы."""
await reorder_wheel_prizes(db, request.prize_ids) await reorder_wheel_prizes(db, request.prize_ids)
logger.info('🔄 Admin reordered prizes', telegram_id=admin.telegram_id, prize_ids=request.prize_ids) logger.info(f'🔄 Admin {admin.telegram_id} reordered prizes: {request.prize_ids}')
return {'success': True} return {'success': True}
+22 -45
View File
@@ -2,9 +2,9 @@
import asyncio import asyncio
import hashlib import hashlib
import logging
from datetime import UTC, datetime from datetime import UTC, datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@@ -69,7 +69,7 @@ from ..services.email_service import email_service
from ..services.email_template_overrides import get_rendered_override from ..services.email_template_overrides import get_rendered_override
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/auth', tags=['Cabinet Auth']) router = APIRouter(prefix='/auth', tags=['Cabinet Auth'])
@@ -158,12 +158,12 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
panel_users = await api.get_user_by_email(user.email) panel_users = await api.get_user_by_email(user.email)
if not panel_users: if not panel_users:
logger.debug('No subscription found in panel for email', email=user.email) logger.debug(f'No subscription found in panel for email: {user.email}')
return return
# Take first user if multiple found # Take first user if multiple found
panel_user = panel_users[0] panel_user = panel_users[0]
logger.info('Found subscription in panel for email', email=user.email, uuid=panel_user.uuid) logger.info(f'Found subscription in panel for email {user.email}: {panel_user.uuid}')
# Link user to panel # Link user to panel
user.remnawave_uuid = panel_user.uuid user.remnawave_uuid = panel_user.uuid
@@ -208,10 +208,7 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
existing_sub.device_limit = device_limit existing_sub.device_limit = device_limit
existing_sub.is_trial = False # Panel subscription is not trial existing_sub.is_trial = False # Panel subscription is not trial
logger.info( logger.info(
'Updated subscription for email user squads: devices', f'Updated subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}'
email=user.email,
connected_squads=connected_squads,
device_limit=device_limit,
) )
else: else:
# Create new subscription (expire_at and current_time already naive UTC) # Create new subscription (expire_at and current_time already naive UTC)
@@ -231,16 +228,13 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
) )
db.add(new_sub) db.add(new_sub)
logger.info( logger.info(
'Created subscription for email user squads: devices', f'Created subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}'
email=user.email,
connected_squads=connected_squads,
device_limit=device_limit,
) )
await db.commit() await db.commit()
except Exception as e: except Exception as e:
logger.warning('Failed to sync subscription from panel for', email=user.email, error=e) logger.warning(f'Failed to sync subscription from panel for {user.email}: {e}')
# Don't rollback - it detaches user object and breaks subsequent operations # Don't rollback - it detaches user object and breaks subsequent operations
# The sync is non-critical, main verification already succeeded # The sync is non-critical, main verification already succeeded
@@ -281,7 +275,7 @@ async def auth_telegram(
if not user: if not user:
# Create new user from Telegram initData # Create new user from Telegram initData
logger.info('Creating new user from cabinet (initData): telegram_id', telegram_id=telegram_id) logger.info(f'Creating new user from cabinet (initData): telegram_id={telegram_id}')
user = await create_user( user = await create_user(
db=db, db=db,
telegram_id=telegram_id, telegram_id=telegram_id,
@@ -290,7 +284,7 @@ async def auth_telegram(
last_name=tg_last_name, last_name=tg_last_name,
language=tg_language, language=tg_language,
) )
logger.info('User created successfully: id=, telegram_id', user_id=user.id, telegram_id=user.telegram_id) logger.info(f'User created successfully: id={user.id}, telegram_id={user.telegram_id}')
else: else:
# Update user info from initData (like bot middleware does) # Update user info from initData (like bot middleware does)
updated = False updated = False
@@ -304,7 +298,7 @@ async def auth_telegram(
user.last_name = tg_last_name user.last_name = tg_last_name
updated = True updated = True
if updated: if updated:
logger.info('User profile updated from initData', user_id=user.id) logger.info(f'User {user.id} profile updated from initData')
if user.status != 'active': if user.status != 'active':
raise HTTPException( raise HTTPException(
@@ -347,9 +341,7 @@ async def auth_telegram_widget(
if not user: if not user:
# Create new user from Telegram data # Create new user from Telegram data
logger.info( logger.info(f'Creating new user from cabinet: telegram_id={request.id}, username={request.username}')
'Creating new user from cabinet: telegram_id=, username', request_id=request.id, username=request.username
)
user = await create_user( user = await create_user(
db=db, db=db,
telegram_id=request.id, telegram_id=request.id,
@@ -358,7 +350,7 @@ async def auth_telegram_widget(
last_name=request.last_name, last_name=request.last_name,
language='ru', language='ru',
) )
logger.info('User created successfully: id=, telegram_id', user_id=user.id, telegram_id=user.telegram_id) logger.info(f'User created successfully: id={user.id}, telegram_id={user.telegram_id}')
if user.status != 'active': if user.status != 'active':
raise HTTPException( raise HTTPException(
@@ -493,7 +485,7 @@ async def register_email_standalone(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail='Invalid test email password', detail='Invalid test email password',
) )
logger.info('Test email registration', email=request.email) logger.info(f'Test email registration: {request.email}')
# Check for disposable email # Check for disposable email
if disposable_email_service.is_disposable(request.email): if disposable_email_service.is_disposable(request.email):
@@ -520,17 +512,11 @@ async def register_email_standalone(
if referrer: if referrer:
# Защита от самореферала - нельзя регистрироваться по своему же коду # Защита от самореферала - нельзя регистрироваться по своему же коду
if referrer.email and referrer.email.lower() == request.email.lower(): if referrer.email and referrer.email.lower() == request.email.lower():
logger.warning( logger.warning(f'Self-referral attempt blocked: email={request.email}, code={request.referral_code}')
'Self-referral attempt blocked: email=, code',
email=request.email,
referral_code=request.referral_code,
)
referrer = None referrer = None
else: else:
logger.info( logger.info(
'Found referrer for email registration: referrer_id=, code', f'Found referrer for email registration: referrer_id={referrer.id}, code={request.referral_code}'
referrer_id=referrer.id,
referral_code=request.referral_code,
) )
# Создать пользователя # Создать пользователя
@@ -548,7 +534,7 @@ async def register_email_standalone(
user.email_verified = True user.email_verified = True
user.email_verified_at = datetime.utcnow() user.email_verified_at = datetime.utcnow()
await db.commit() await db.commit()
logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id) logger.info(f'Test email auto-verified: {request.email}, user_id={user.id}')
else: else:
# Сгенерировать токен верификации # Сгенерировать токен верификации
verification_token = generate_verification_token() verification_token = generate_verification_token()
@@ -593,11 +579,9 @@ async def register_email_standalone(
if referrer: if referrer:
try: try:
await process_referral_registration(db, user.id, referrer.id, bot=None) await process_referral_registration(db, user.id, referrer.id, bot=None)
logger.info( logger.info(f'Processed referral registration: user_id={user.id}, referrer_id={referrer.id}')
'Processed referral registration: user_id=, referrer_id', user_id=user.id, referrer_id=referrer.id
)
except Exception as e: except Exception as e:
logger.error('Failed to process referral registration', error=e) logger.error(f'Failed to process referral registration: {e}')
# Не прерываем регистрацию из-за ошибки реферальной системы # Не прерываем регистрацию из-за ошибки реферальной системы
# Для тестового email - сразу можно логиниться (уже verified) # Для тестового email - сразу можно логиниться (уже verified)
@@ -740,7 +724,7 @@ async def login_email(
if not user: if not user:
# For test email - auto-create user if not exists # For test email - auto-create user if not exists
if is_test_email and settings.validate_test_email_password(request.email, request.password): if is_test_email and settings.validate_test_email_password(request.email, request.password):
logger.info('Test email login creating new user', email=request.email) logger.info(f'Test email login - creating new user: {request.email}')
password_hash = hash_password(request.password) password_hash = hash_password(request.password)
user = await create_user_by_email( user = await create_user_by_email(
db=db, db=db,
@@ -1067,16 +1051,9 @@ async def request_email_change(
custom_body_html=custom_body, custom_body_html=custom_body,
) )
except Exception as e: except Exception as e:
logger.error( logger.error(f'Failed to send verification email to {request.new_email} for user {user.id}: {e}')
'Failed to send verification email to for user',
new_email=request.new_email,
user_id=user.id,
error=e,
)
logger.info( logger.info(f'Unverified email replaced for user {user.id}: {old_email} -> {request.new_email}')
'Unverified email replaced for user', user_id=user.id, old_email=old_email, new_email=request.new_email
)
return EmailChangeResponse( return EmailChangeResponse(
message='Email replaced, verification sent to new address', message='Email replaced, verification sent to new address',
@@ -1127,7 +1104,7 @@ async def request_email_change(
detail='Email service is not configured', detail='Email service is not configured',
) )
logger.info('Email change requested for user', user_id=user.id, email=user.email, new_email=request.new_email) logger.info(f'Email change requested for user {user.id}: {user.email} -> {request.new_email}')
return EmailChangeResponse( return EmailChangeResponse(
message='Verification code sent to new email', message='Verification code sent to new email',
+8 -10
View File
@@ -1,11 +1,11 @@
"""Balance and payment routes for cabinet.""" """Balance and payment routes for cabinet."""
import logging
import math import math
import time import time
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
import httpx import httpx
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import desc, func, select from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -41,7 +41,7 @@ from ..schemas.balance import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/balance', tags=['Cabinet Balance']) router = APIRouter(prefix='/balance', tags=['Cabinet Balance'])
@@ -249,7 +249,7 @@ async def create_stars_invoice(
if stars_amount <= 0: if stars_amount <= 0:
stars_amount = 1 stars_amount = 1
except Exception as e: except Exception as e:
logger.error('Error calculating Stars amount', error=e) logger.error(f'Error calculating Stars amount: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to calculate Stars amount', detail='Failed to calculate Stars amount',
@@ -279,7 +279,7 @@ async def create_stars_invoice(
result = response.json() result = response.json()
if not result.get('ok'): if not result.get('ok'):
logger.error('Telegram API error', result=result) logger.error(f'Telegram API error: {result}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to create Stars invoice', detail='Failed to create Stars invoice',
@@ -287,10 +287,8 @@ async def create_stars_invoice(
invoice_url = result['result'] invoice_url = result['result']
logger.info( logger.info(
'Created Stars invoice for balance top-up: user=, amount= kopeks, stars', f'Created Stars invoice for balance top-up: user={user.id}, '
user_id=user.id, f'amount={request.amount_kopeks} kopeks, stars={stars_amount}'
amount_kopeks=request.amount_kopeks,
stars_amount=stars_amount,
) )
return StarsInvoiceResponse( return StarsInvoiceResponse(
@@ -300,7 +298,7 @@ async def create_stars_invoice(
) )
except httpx.HTTPError as e: except httpx.HTTPError as e:
logger.error('HTTP error creating Stars invoice', error=e) logger.error(f'HTTP error creating Stars invoice: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to connect to Telegram API', detail='Failed to connect to Telegram API',
@@ -712,7 +710,7 @@ async def create_topup(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Payment creation error', error=e) logger.error(f'Payment creation error: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to create payment. Please try again later.', detail='Failed to create payment. Please try again later.',
+13 -13
View File
@@ -1,10 +1,10 @@
"""Branding routes for cabinet - logo, project name, and theme colors management.""" """Branding routes for cabinet - logo, project name, and theme colors management."""
import json import json
import logging
import os import os
from pathlib import Path from pathlib import Path
import structlog
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel from pydantic import BaseModel
@@ -17,7 +17,7 @@ from app.database.models import SystemSetting, User
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/branding', tags=['Branding']) router = APIRouter(prefix='/branding', tags=['Branding'])
@@ -307,7 +307,7 @@ async def update_branding_name(
await set_setting_value(db, BRANDING_NAME_KEY, name) await set_setting_value(db, BRANDING_NAME_KEY, name)
logger.info('Admin updated branding name to', telegram_id=admin.telegram_id, name=name) logger.info(f'Admin {admin.telegram_id} updated branding name to: {name}')
# Return updated branding # Return updated branding
custom_logo = has_custom_logo() custom_logo = has_custom_logo()
@@ -368,7 +368,7 @@ async def upload_logo(
# Mark that we have a custom logo # Mark that we have a custom logo
await set_setting_value(db, BRANDING_LOGO_KEY, 'custom') await set_setting_value(db, BRANDING_LOGO_KEY, 'custom')
logger.info('Admin uploaded new logo', telegram_id=admin.telegram_id, logo_path=logo_path) logger.info(f'Admin {admin.telegram_id} uploaded new logo: {logo_path}')
# Get current name for response # Get current name for response
name = await get_setting_value(db, BRANDING_NAME_KEY) name = await get_setting_value(db, BRANDING_NAME_KEY)
@@ -398,7 +398,7 @@ async def delete_logo(
# Update setting # Update setting
await set_setting_value(db, BRANDING_LOGO_KEY, 'default') await set_setting_value(db, BRANDING_LOGO_KEY, 'default')
logger.info('Admin deleted custom logo', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} deleted custom logo')
# Get current name for response # Get current name for response
name = await get_setting_value(db, BRANDING_NAME_KEY) name = await get_setting_value(db, BRANDING_NAME_KEY)
@@ -486,7 +486,7 @@ async def update_theme_colors(
# Save to database # Save to database
await set_setting_value(db, THEME_COLORS_KEY, json.dumps(current_colors)) await set_setting_value(db, THEME_COLORS_KEY, json.dumps(current_colors))
logger.info('Admin updated theme colors', telegram_id=admin.telegram_id, value=list(update_data.keys())) logger.info(f'Admin {admin.telegram_id} updated theme colors: {list(update_data.keys())}')
return ThemeColorsResponse(**current_colors) return ThemeColorsResponse(**current_colors)
@@ -500,7 +500,7 @@ async def reset_theme_colors(
# Save default colors # Save default colors
await set_setting_value(db, THEME_COLORS_KEY, json.dumps(DEFAULT_THEME_COLORS)) await set_setting_value(db, THEME_COLORS_KEY, json.dumps(DEFAULT_THEME_COLORS))
logger.info('Admin reset theme colors to defaults', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} reset theme colors to defaults')
return ThemeColorsResponse(**DEFAULT_THEME_COLORS) return ThemeColorsResponse(**DEFAULT_THEME_COLORS)
@@ -558,7 +558,7 @@ async def update_enabled_themes(
# Save to database # Save to database
await set_setting_value(db, ENABLED_THEMES_KEY, json.dumps(current_themes)) await set_setting_value(db, ENABLED_THEMES_KEY, json.dumps(current_themes))
logger.info('Admin updated enabled themes', telegram_id=admin.telegram_id, current_themes=current_themes) logger.info(f'Admin {admin.telegram_id} updated enabled themes: {current_themes}')
return EnabledThemesResponse(**current_themes) return EnabledThemesResponse(**current_themes)
@@ -593,7 +593,7 @@ async def update_animation_enabled(
"""Update animation enabled setting. Admin only.""" """Update animation enabled setting. Admin only."""
await set_setting_value(db, ANIMATION_ENABLED_KEY, str(payload.enabled).lower()) await set_setting_value(db, ANIMATION_ENABLED_KEY, str(payload.enabled).lower())
logger.info('Admin set animation enabled', telegram_id=admin.telegram_id, enabled=payload.enabled) logger.info(f'Admin {admin.telegram_id} set animation enabled: {payload.enabled}')
return AnimationEnabledResponse(enabled=payload.enabled) return AnimationEnabledResponse(enabled=payload.enabled)
@@ -628,7 +628,7 @@ async def update_fullscreen_enabled(
"""Update fullscreen enabled setting. Admin only.""" """Update fullscreen enabled setting. Admin only."""
await set_setting_value(db, FULLSCREEN_ENABLED_KEY, str(payload.enabled).lower()) await set_setting_value(db, FULLSCREEN_ENABLED_KEY, str(payload.enabled).lower())
logger.info('Admin set fullscreen enabled', telegram_id=admin.telegram_id, enabled=payload.enabled) logger.info(f'Admin {admin.telegram_id} set fullscreen enabled: {payload.enabled}')
return FullscreenEnabledResponse(enabled=payload.enabled) return FullscreenEnabledResponse(enabled=payload.enabled)
@@ -664,7 +664,7 @@ async def update_email_auth_enabled(
"""Update email auth enabled setting. Admin only.""" """Update email auth enabled setting. Admin only."""
await set_setting_value(db, EMAIL_AUTH_ENABLED_KEY, str(payload.enabled).lower()) await set_setting_value(db, EMAIL_AUTH_ENABLED_KEY, str(payload.enabled).lower())
logger.info('Admin set email auth enabled', telegram_id=admin.telegram_id, enabled=payload.enabled) logger.info(f'Admin {admin.telegram_id} set email auth enabled: {payload.enabled}')
return EmailAuthEnabledResponse(enabled=payload.enabled) return EmailAuthEnabledResponse(enabled=payload.enabled)
@@ -719,7 +719,7 @@ async def update_analytics_counters(
if payload.google_ads_label is not None: if payload.google_ads_label is not None:
await set_setting_value(db, GOOGLE_ADS_LABEL_KEY, payload.google_ads_label.strip()) await set_setting_value(db, GOOGLE_ADS_LABEL_KEY, payload.google_ads_label.strip())
logger.info('Admin updated analytics counters', telegram_id=admin.telegram_id) logger.info(f'Admin {admin.telegram_id} updated analytics counters')
# Return current state # Return current state
yandex_id = await get_setting_value(db, YANDEX_METRIKA_ID_KEY) or '' yandex_id = await get_setting_value(db, YANDEX_METRIKA_ID_KEY) or ''
@@ -764,6 +764,6 @@ async def update_lite_mode_enabled(
"""Update lite mode enabled setting. Admin only.""" """Update lite mode enabled setting. Admin only."""
await set_setting_value(db, LITE_MODE_ENABLED_KEY, str(payload.enabled).lower()) await set_setting_value(db, LITE_MODE_ENABLED_KEY, str(payload.enabled).lower())
logger.info('Admin set lite mode enabled', telegram_id=admin.telegram_id, enabled=payload.enabled) logger.info(f'Admin {admin.telegram_id} set lite mode enabled: {payload.enabled}')
return LiteModeEnabledResponse(enabled=payload.enabled) return LiteModeEnabledResponse(enabled=payload.enabled)
+5 -5
View File
@@ -1,10 +1,10 @@
"""Contests routes for cabinet - user participation in games/contests.""" """Contests routes for cabinet - user participation in games/contests."""
import logging
import random import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -30,7 +30,7 @@ from app.services.contest_rotation_service import (
from ..dependencies import get_cabinet_db, get_current_cabinet_user from ..dependencies import get_cabinet_db, get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/contests', tags=['Cabinet Contests']) router = APIRouter(prefix='/contests', tags=['Cabinet Contests'])
@@ -106,7 +106,7 @@ async def _award_prize(db: AsyncSession, user_id: int, prize_type: str, prize_va
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info('🎁 Extended subscription for user by days (contest prize)', user_id=user_id, days=days) logger.info(f'🎁 Extended subscription for user {user_id} by {days} days (contest prize)')
return f'Subscription extended by {days} days' return f'Subscription extended by {days} days'
if prize_type == 'balance': if prize_type == 'balance':
@@ -125,10 +125,10 @@ async def _award_prize(db: AsyncSession, user_id: int, prize_type: str, prize_va
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
logger.info('🎁 Added to balance for user (contest prize)', amount=amount, user_id=user_id) logger.info(f'🎁 Added {amount} to balance for user {user_id} (contest prize)')
return f'Balance increased by {amount}' return f'Balance increased by {amount}'
logger.warning('Unknown prize type', prize_type=prize_type) logger.warning(f'Unknown prize type: {prize_type}')
return f"Prize type '{prize_type}' not supported" return f"Prize type '{prize_type}' not supported"
+3 -2
View File
@@ -1,6 +1,7 @@
"""Info pages routes for cabinet - FAQ, rules, privacy policy, etc.""" """Info pages routes for cabinet - FAQ, rules, privacy policy, etc."""
import structlog import logging
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -15,7 +16,7 @@ from app.services.public_offer_service import PublicOfferService
from ..dependencies import get_cabinet_db, get_current_cabinet_user from ..dependencies import get_cabinet_db, get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/info', tags=['Cabinet Info']) router = APIRouter(prefix='/info', tags=['Cabinet Info'])
+5 -10
View File
@@ -1,8 +1,8 @@
"""Media upload/download routes for cabinet tickets.""" """Media upload/download routes for cabinet tickets."""
import logging
import mimetypes import mimetypes
import structlog
from aiogram import Bot from aiogram import Bot
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
@@ -16,7 +16,7 @@ from app.database.models import User
from ..dependencies import get_current_cabinet_user from ..dependencies import get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/media', tags=['Cabinet Media']) router = APIRouter(prefix='/media', tags=['Cabinet Media'])
@@ -125,12 +125,7 @@ async def upload_media(
media_url = _build_media_url(request, media.file_id) media_url = _build_media_url(request, media.file_id)
logger.info( logger.info(f'User {user.telegram_id} uploaded {media_type_normalized}: {media.file_id}')
'User uploaded',
telegram_id=user.telegram_id,
media_type_normalized=media_type_normalized,
file_id=media.file_id,
)
return MediaUploadResponse( return MediaUploadResponse(
media_type=media_type_normalized, media_type=media_type_normalized,
@@ -141,7 +136,7 @@ async def upload_media(
except HTTPException: except HTTPException:
raise raise
except Exception as error: except Exception as error:
logger.error('Failed to upload media for user', telegram_id=user.telegram_id, error=error) logger.error(f'Failed to upload media for user {user.telegram_id}: {error}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to upload media', detail='Failed to upload media',
@@ -192,7 +187,7 @@ async def download_media(
except HTTPException: except HTTPException:
raise raise
except Exception as error: except Exception as error:
logger.error('Failed to download media', file_id=file_id, error=error) logger.error(f'Failed to download media {file_id}: {error}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to download media', detail='Failed to download media',
+2 -2
View File
@@ -1,9 +1,9 @@
"""Notification settings routes for cabinet.""" """Notification settings routes for cabinet."""
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +13,7 @@ from app.database.models import User
from ..dependencies import get_cabinet_db, get_current_cabinet_user from ..dependencies import get_cabinet_db, get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/notifications', tags=['Cabinet Notifications']) router = APIRouter(prefix='/notifications', tags=['Cabinet Notifications'])
+7 -7
View File
@@ -1,8 +1,8 @@
"""OAuth 2.0 authentication routes for cabinet.""" """OAuth 2.0 authentication routes for cabinet."""
import logging
from datetime import UTC, datetime from datetime import UTC, datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -27,7 +27,7 @@ from ..schemas.auth import AuthResponse
from .auth import _create_auth_response, _store_refresh_token from .auth import _create_auth_response, _store_refresh_token
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth']) router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth'])
@@ -120,7 +120,7 @@ async def oauth_callback(
try: try:
token_data = await oauth_provider.exchange_code(request.code) token_data = await oauth_provider.exchange_code(request.code)
except Exception as exc: except Exception as exc:
logger.error('OAuth code exchange failed for', provider=provider, exc=exc) logger.error('OAuth code exchange failed for %s: %s', provider, exc)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail='Failed to exchange authorization code', detail='Failed to exchange authorization code',
@@ -130,7 +130,7 @@ async def oauth_callback(
try: try:
user_info: OAuthUserInfo = await oauth_provider.get_user_info(token_data) user_info: OAuthUserInfo = await oauth_provider.get_user_info(token_data)
except Exception as exc: except Exception as exc:
logger.error('OAuth user info fetch failed for', provider=provider, exc=exc) logger.error('OAuth user info fetch failed for %s: %s', provider, exc)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail='Failed to fetch user information from provider', detail='Failed to fetch user information from provider',
@@ -139,7 +139,7 @@ async def oauth_callback(
# 5. Find user by provider ID # 5. Find user by provider ID
user = await get_user_by_oauth_provider(db, provider, user_info.provider_id) user = await get_user_by_oauth_provider(db, provider, user_info.provider_id)
if user: if user:
logger.info('OAuth login via for existing user', provider=provider, user_id=user.id) logger.info('OAuth login via %s for existing user %s', provider, user.id)
return await _finalize_oauth_login(db, user, provider) return await _finalize_oauth_login(db, user, provider)
# 6. Find user by email (if verified) and link provider # 6. Find user by email (if verified) and link provider
@@ -147,7 +147,7 @@ async def oauth_callback(
user = await get_user_by_email(db, user_info.email) user = await get_user_by_email(db, user_info.email)
if user: if user:
await set_user_oauth_provider_id(db, user, provider, user_info.provider_id) await set_user_oauth_provider_id(db, user, provider, user_info.provider_id)
logger.info('OAuth login via linked to existing email user', provider=provider, user_id=user.id) logger.info('OAuth login via %s linked to existing email user %s', provider, user.id)
return await _finalize_oauth_login(db, user, provider) return await _finalize_oauth_login(db, user, provider)
# 7. Create new user # 7. Create new user
@@ -161,5 +161,5 @@ async def oauth_callback(
last_name=user_info.last_name, last_name=user_info.last_name,
username=user_info.username, username=user_info.username,
) )
logger.info('OAuth new user created via with id', provider=provider, user_id=user.id) logger.info('OAuth new user created via %s with id=%s', provider, user.id)
return await _finalize_oauth_login(db, user, provider) return await _finalize_oauth_login(db, user, provider)
+2 -2
View File
@@ -1,8 +1,8 @@
"""Polls routes for cabinet - user participation in polls/surveys.""" """Polls routes for cabinet - user participation in polls/surveys."""
import logging
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import select from sqlalchemy import select
@@ -20,7 +20,7 @@ from app.services.poll_service import get_next_question, get_question_option, re
from ..dependencies import get_cabinet_db, get_current_cabinet_user from ..dependencies import get_cabinet_db, get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/polls', tags=['Cabinet Polls']) router = APIRouter(prefix='/polls', tags=['Cabinet Polls'])
+2 -2
View File
@@ -1,9 +1,9 @@
"""Promo offers routes for cabinet - personal discounts and offers.""" """Promo offers routes for cabinet - personal discounts and offers."""
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import and_, select from sqlalchemy import and_, select
@@ -22,7 +22,7 @@ from app.services.promo_offer_service import promo_offer_service
from ..dependencies import get_cabinet_db, get_current_cabinet_user from ..dependencies import get_cabinet_db, get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/promo', tags=['Cabinet Promo']) router = APIRouter(prefix='/promo', tags=['Cabinet Promo'])
+3 -2
View File
@@ -1,6 +1,7 @@
"""Promo code routes for cabinet.""" """Promo code routes for cabinet."""
import structlog import logging
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,7 +12,7 @@ from app.services.promocode_service import PromoCodeService
from ..dependencies import get_cabinet_db, get_current_cabinet_user from ..dependencies import get_cabinet_db, get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/promocode', tags=['Cabinet Promocode']) router = APIRouter(prefix='/promocode', tags=['Cabinet Promocode'])
+2 -2
View File
@@ -1,8 +1,8 @@
"""Referral program routes for cabinet.""" """Referral program routes for cabinet."""
import logging
import math import math
import structlog
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy import desc, func, select from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,7 +22,7 @@ from ..schemas.referral import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/referral', tags=['Cabinet Referral']) router = APIRouter(prefix='/referral', tags=['Cabinet Referral'])
+79 -108
View File
@@ -2,11 +2,11 @@
import base64 import base64
import json import json
import logging
import re import re
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -58,7 +58,7 @@ from ..schemas.subscription import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/subscription', tags=['Cabinet Subscription']) router = APIRouter(prefix='/subscription', tags=['Cabinet Subscription'])
@@ -490,9 +490,9 @@ async def renew_subscription(
try: try:
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info('Cart saved for auto-renewal (cabinet) user', user_id=user.id) logger.info(f'Cart saved for auto-renewal (cabinet) user {user.id}')
except Exception as e: except Exception as e:
logger.error('Error saving cart for auto-renewal (cabinet)', error=e) logger.error(f'Error saving cart for auto-renewal (cabinet): {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED, status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -550,7 +550,7 @@ async def renew_subscription(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send admin notification for subscription renewal', error=e) logger.error(f'Failed to send admin notification for subscription renewal: {e}')
response = { response = {
'message': 'Subscription renewed successfully', 'message': 'Subscription renewed successfully',
@@ -789,13 +789,11 @@ async def purchase_traffic(
try: try:
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info( logger.info(
'Cart saved for traffic purchase (cabinet) user + discount', f'Cart saved for traffic purchase (cabinet) user {user.id}: '
user_id=user.id, f'+{request.gb} GB, discount {traffic_discount_percent}%'
gb=request.gb,
traffic_discount_percent=traffic_discount_percent,
) )
except Exception as e: except Exception as e:
logger.error('Error saving cart for traffic purchase (cabinet)', error=e) logger.error(f'Error saving cart for traffic purchase (cabinet): {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED, status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -835,11 +833,7 @@ async def purchase_traffic(
from datetime import timedelta from datetime import timedelta
subscription.traffic_reset_at = datetime.utcnow() + timedelta(days=30) subscription.traffic_reset_at = datetime.utcnow() + timedelta(days=30)
logger.info( logger.info(f'Set traffic_reset_at for subscription {subscription.id}: {subscription.traffic_reset_at}')
'Set traffic_reset_at for subscription',
subscription_id=subscription.id,
traffic_reset_at=subscription.traffic_reset_at,
)
await db.commit() await db.commit()
@@ -851,7 +845,7 @@ async def purchase_traffic(
else: else:
await subscription_service.create_remnawave_user(db, subscription) await subscription_service.create_remnawave_user(db, subscription)
except Exception as e: except Exception as e:
logger.error('Failed to sync traffic with RemnaWave', error=e) logger.error(f'Failed to sync traffic with RemnaWave: {e}')
# Создаём транзакцию # Создаём транзакцию
await create_transaction( await create_transaction(
@@ -888,7 +882,7 @@ async def purchase_traffic(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send admin notification for traffic purchase', error=e) logger.error(f'Failed to send admin notification for traffic purchase: {e}')
response = { response = {
'success': True, 'success': True,
@@ -952,13 +946,9 @@ async def purchase_devices_legacy(
'source': 'cabinet', 'source': 'cabinet',
} }
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info( logger.info(f'Cart saved for device purchase (cabinet /devices) user {user.id}: +{request.devices} devices')
'Cart saved for device purchase (cabinet /devices) user + devices',
user_id=user.id,
devices=request.devices,
)
except Exception as e: except Exception as e:
logger.error('Error saving cart for device purchase (cabinet /devices)', error=e) logger.error(f'Error saving cart for device purchase (cabinet /devices): {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED, status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -1030,7 +1020,7 @@ async def purchase_devices_legacy(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send admin notification for device purchase', error=e) logger.error(f'Failed to send admin notification for device purchase: {e}')
response = { response = {
'message': 'Devices added successfully', 'message': 'Devices added successfully',
@@ -1133,7 +1123,7 @@ async def get_trial_info(
if tariff_trial_days: if tariff_trial_days:
duration_days = tariff_trial_days duration_days = tariff_trial_days
except Exception as e: except Exception as e:
logger.error('Error getting trial tariff for info', error=e) logger.error(f'Error getting trial tariff for info: {e}')
# Check if user already has an active subscription # Check if user already has an active subscription
if user.subscription: if user.subscription:
@@ -1221,7 +1211,7 @@ async def activate_trial(
detail=f'Insufficient balance. Need {price_kopeks / 100:.2f} RUB', detail=f'Insufficient balance. Need {price_kopeks / 100:.2f} RUB',
) )
user.balance_kopeks -= price_kopeks user.balance_kopeks -= price_kopeks
logger.info('User paid kopeks for trial activation', user_id=user.id, price_kopeks=price_kopeks) logger.info(f'User {user.id} paid {price_kopeks} kopeks for trial activation')
# Get trial parameters from tariff if configured (same logic as bot handler) # Get trial parameters from tariff if configured (same logic as bot handler)
trial_duration = settings.TRIAL_DURATION_DAYS trial_duration = settings.TRIAL_DURATION_DAYS
@@ -1253,14 +1243,9 @@ async def activate_trial(
tariff_trial_days = getattr(trial_tariff, 'trial_duration_days', None) tariff_trial_days = getattr(trial_tariff, 'trial_duration_days', None)
if tariff_trial_days: if tariff_trial_days:
trial_duration = tariff_trial_days trial_duration = tariff_trial_days
logger.info( logger.info(f'Using trial tariff {trial_tariff.name} (ID: {trial_tariff.id}) with squads: {trial_squads}')
'Using trial tariff (ID: ) with squads',
trial_tariff_name=trial_tariff.name,
trial_tariff_id=trial_tariff.id,
trial_squads=trial_squads,
)
except Exception as e: except Exception as e:
logger.error('Error getting trial tariff', error=e) logger.error(f'Error getting trial tariff: {e}')
# Create trial subscription # Create trial subscription
subscription = await create_trial_subscription( subscription = await create_trial_subscription(
@@ -1273,7 +1258,7 @@ async def activate_trial(
tariff_id=tariff_id_for_trial, tariff_id=tariff_id_for_trial,
) )
logger.info('Trial subscription activated for user', user_id=user.id) logger.info(f'Trial subscription activated for user {user.id}')
# Create RemnaWave user # Create RemnaWave user
try: try:
@@ -1282,7 +1267,7 @@ async def activate_trial(
await subscription_service.create_remnawave_user(db, subscription) await subscription_service.create_remnawave_user(db, subscription)
await db.refresh(subscription) await db.refresh(subscription)
except Exception as e: except Exception as e:
logger.error('Failed to create RemnaWave user for trial', error=e) logger.error(f'Failed to create RemnaWave user for trial: {e}')
# Send admin notification about trial activation # Send admin notification about trial activation
try: try:
@@ -1301,7 +1286,7 @@ async def activate_trial(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send trial activation notification', error=e) logger.error(f'Failed to send trial activation notification: {e}')
return _subscription_to_response(subscription) return _subscription_to_response(subscription)
@@ -1566,7 +1551,7 @@ async def get_purchase_options(
detail=str(e), detail=str(e),
) )
except Exception as e: except Exception as e:
logger.error('Failed to build purchase options for user', user_id=user.id, error=e) logger.error(f'Failed to build purchase options for user {user.id}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load purchase options', detail='Failed to load purchase options',
@@ -1611,7 +1596,7 @@ async def preview_purchase(
detail=str(e), detail=str(e),
) )
except Exception as e: except Exception as e:
logger.error('Failed to calculate purchase preview for user', user_id=user.id, error=e) logger.error(f'Failed to calculate purchase preview for user {user.id}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to calculate price', detail='Failed to calculate price',
@@ -1673,7 +1658,7 @@ async def submit_purchase(
bot=None, bot=None,
) )
except Exception as notif_error: except Exception as notif_error:
logger.warning('Failed to send subscription notification to', email=user.email, notif_error=notif_error) logger.warning(f'Failed to send subscription notification to {user.email}: {notif_error}')
# Отправляем уведомление админам о покупке подписки # Отправляем уведомление админам о покупке подписки
try: try:
@@ -1699,7 +1684,7 @@ async def submit_purchase(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send admin notification for subscription purchase', error=e) logger.error(f'Failed to send admin notification for subscription purchase: {e}')
return { return {
'success': True, 'success': True,
@@ -1731,9 +1716,9 @@ async def submit_purchase(
'source': 'cabinet', 'source': 'cabinet',
} }
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info('Cart saved for auto-purchase (cabinet /purchase) user', user_id=user.id) logger.info(f'Cart saved for auto-purchase (cabinet /purchase) user {user.id}')
except Exception as cart_error: except Exception as cart_error:
logger.error('Error saving cart for auto-purchase (cabinet /purchase)', cart_error=cart_error) logger.error(f'Error saving cart for auto-purchase (cabinet /purchase): {cart_error}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED, status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -1745,7 +1730,7 @@ async def submit_purchase(
}, },
) )
except Exception as e: except Exception as e:
logger.error('Failed to submit purchase for user', user_id=user.id, error=e) logger.error(f'Failed to submit purchase for user {user.id}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to process purchase', detail='Failed to process purchase',
@@ -1927,9 +1912,9 @@ async def purchase_tariff(
try: try:
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info('Cart saved for auto-purchase (cabinet) user tariff', user_id=user.id, tariff_id=tariff.id) logger.info(f'Cart saved for auto-purchase (cabinet) user {user.id}, tariff {tariff.id}')
except Exception as e: except Exception as e:
logger.error('Error saving cart for auto-purchase (cabinet)', error=e) logger.error(f'Error saving cart for auto-purchase (cabinet): {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED, status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -2034,7 +2019,7 @@ async def purchase_tariff(
reset_reason='покупка тарифа (cabinet)', reset_reason='покупка тарифа (cabinet)',
) )
except Exception as remnawave_error: except Exception as remnawave_error:
logger.error('Failed to sync subscription with RemnaWave', remnawave_error=remnawave_error) logger.error(f'Failed to sync subscription with RemnaWave: {remnawave_error}')
# Save cart for auto-renewal (not for daily tariffs - they have their own charging) # Save cart for auto-renewal (not for daily tariffs - they have their own charging)
if not is_daily_tariff: if not is_daily_tariff:
@@ -2048,9 +2033,9 @@ async def purchase_tariff(
'description': f'Продление тарифа {tariff.name} на {period_days} дней', 'description': f'Продление тарифа {tariff.name} на {period_days} дней',
} }
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info('Tariff cart saved for auto-renewal (cabinet) user', user_id=user.id) logger.info(f'Tariff cart saved for auto-renewal (cabinet) user {user.id}')
except Exception as e: except Exception as e:
logger.error('Error saving tariff cart (cabinet)', error=e) logger.error(f'Error saving tariff cart (cabinet): {e}')
await db.refresh(user) await db.refresh(user)
@@ -2110,7 +2095,7 @@ async def purchase_tariff(
bot=None, bot=None,
) )
except Exception as notif_error: except Exception as notif_error:
logger.warning('Failed to send subscription notification to', email=user.email, notif_error=notif_error) logger.warning(f'Failed to send subscription notification to {user.email}: {notif_error}')
# Отправляем уведомление админам о покупке/продлении тарифа # Отправляем уведомление админам о покупке/продлении тарифа
try: try:
@@ -2139,14 +2124,14 @@ async def purchase_tariff(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send admin notification for tariff purchase', error=e) logger.error(f'Failed to send admin notification for tariff purchase: {e}')
return response return response
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Failed to purchase tariff for user', user_id=user.id, error=e) logger.error(f'Failed to purchase tariff for user {user.id}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to process tariff purchase', detail='Failed to process tariff purchase',
@@ -2253,13 +2238,11 @@ async def purchase_devices(
} }
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info( logger.info(
'Cart saved for device purchase (cabinet) user + devices, discount', f'Cart saved for device purchase (cabinet) user {user.id}: '
user_id=user.id, f'+{request.devices} devices, discount {devices_discount_percent}%'
devices=request.devices,
devices_discount_percent=devices_discount_percent,
) )
except Exception as e: except Exception as e:
logger.error('Error saving cart for device purchase (cabinet)', error=e) logger.error(f'Error saving cart for device purchase (cabinet): {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED, status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -2305,23 +2288,17 @@ async def purchase_devices(
else: else:
await service.create_remnawave_user(db, subscription) await service.create_remnawave_user(db, subscription)
except Exception as e: except Exception as e:
logger.error('Failed to sync devices with RemnaWave', error=e) logger.error(f'Failed to sync devices with RemnaWave: {e}')
await db.refresh(user) await db.refresh(user)
if devices_discount_percent > 0: if devices_discount_percent > 0:
logger.info( logger.info(
'User purchased devices for kopeks (discount saved kopeks)', f'User {user.id} purchased {request.devices} devices for {price_kopeks} kopeks '
user_id=user.id, f'(discount {devices_discount_percent}%, saved {discount_value} kopeks)'
devices=request.devices,
price_kopeks=price_kopeks,
devices_discount_percent=devices_discount_percent,
discount_value=discount_value,
) )
else: else:
logger.info( logger.info(f'User {user.id} purchased {request.devices} devices for {price_kopeks} kopeks')
'User purchased devices for kopeks', user_id=user.id, devices=request.devices, price_kopeks=price_kopeks
)
# Отправляем уведомление админам # Отправляем уведомление админам
try: try:
@@ -2345,7 +2322,7 @@ async def purchase_devices(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send admin notification for device purchase', error=e) logger.error(f'Failed to send admin notification for device purchase: {e}')
response = { response = {
'success': True, 'success': True,
@@ -2368,7 +2345,7 @@ async def purchase_devices(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error('Failed to purchase devices for user', user_id=user.id, error=e) logger.error(f'Failed to purchase devices for user {user.id}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Не удалось обработать покупку устройств', detail='Не удалось обработать покупку устройств',
@@ -2486,7 +2463,7 @@ async def save_traffic_cart(
'description': f'Докупка {request.gb} ГБ трафика', 'description': f'Докупка {request.gb} ГБ трафика',
} }
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info('Cart saved for traffic purchase (cabinet save-cart) user +', user_id=user.id, gb=request.gb) logger.info(f'Cart saved for traffic purchase (cabinet save-cart) user {user.id}: +{request.gb} GB')
return {'success': True, 'cart_saved': True} return {'success': True, 'cart_saved': True}
@@ -2561,9 +2538,7 @@ async def save_devices_cart(
'source': 'cabinet', 'source': 'cabinet',
} }
await user_cart_service.save_user_cart(user.id, cart_data) await user_cart_service.save_user_cart(user.id, cart_data)
logger.info( logger.info(f'Cart saved for device purchase (cabinet save-cart) user {user.id}: +{request.devices} devices')
'Cart saved for device purchase (cabinet save-cart) user + devices', user_id=user.id, devices=request.devices
)
return {'success': True, 'cart_saved': True} return {'success': True, 'cart_saved': True}
@@ -2689,7 +2664,7 @@ def _load_app_config_from_file() -> dict[str, Any]:
if isinstance(data, dict): if isinstance(data, dict):
return data return data
except Exception as e: except Exception as e:
logger.error('Failed to load app-config.json', error=e) logger.error(f'Failed to load app-config.json: {e}')
return {} return {}
@@ -2832,11 +2807,10 @@ def _get_url_scheme_for_app(app: dict[str, Any]) -> tuple[str, bool]:
# No scheme found # No scheme found
logger.debug( logger.debug(
'_get_url_scheme_for_app: No scheme found for app has blocks: has buttons: has urlScheme', f"_get_url_scheme_for_app: No scheme found for app '{app.get('name')}', "
get=app.get('name'), f'has blocks: {bool(app.get("blocks"))}, '
get_2=bool(app.get('blocks')), f'has buttons: {bool(app.get("buttons"))}, '
get_3=bool(app.get('buttons')), f'has urlScheme: {bool(app.get("urlScheme"))}'
get_4=bool(app.get('urlScheme')),
) )
return '', False return '', False
@@ -2882,7 +2856,7 @@ def _convert_remnawave_app_to_cabinet(app: dict[str, Any]) -> dict[str, Any]:
# Debug log for conversion (не логируем отсутствие urlScheme - для Happ это нормально) # Debug log for conversion (не логируем отсутствие urlScheme - для Happ это нормально)
app_name = app.get('name', 'unknown') app_name = app.get('name', 'unknown')
if url_scheme: if url_scheme:
logger.debug('_convert_remnawave_app_to_cabinet: app urlScheme', app_name=app_name, url_scheme=url_scheme) logger.debug(f"_convert_remnawave_app_to_cabinet: app '{app_name}' -> urlScheme='{url_scheme}'")
# Smart block mapping: find blocks by their content, not just position # Smart block mapping: find blocks by their content, not just position
# 1. First block is usually installation # 1. First block is usually installation
@@ -2991,12 +2965,12 @@ async def _load_app_config_async() -> dict[str, Any]:
async with service.get_api_client() as api: async with service.get_api_client() as api:
config = await api.get_subscription_page_config(remnawave_uuid) config = await api.get_subscription_page_config(remnawave_uuid)
if config and config.config: if config and config.config:
logger.debug('Loaded app config from RemnaWave', remnawave_uuid=remnawave_uuid) logger.debug(f'Loaded app config from RemnaWave: {remnawave_uuid}')
raw = dict(config.config) raw = dict(config.config)
raw['_isRemnawave'] = True raw['_isRemnawave'] = True
return raw return raw
except Exception as e: except Exception as e:
logger.warning('Failed to load RemnaWave config, falling back to file', error=e) logger.warning(f'Failed to load RemnaWave config, falling back to file: {e}')
# Fallback to local file # Fallback to local file
return _load_app_config_from_file() return _load_app_config_from_file()
@@ -3027,21 +3001,21 @@ def _create_deep_link(
scheme, uses_crypto = _get_url_scheme_for_app(app) scheme, uses_crypto = _get_url_scheme_for_app(app)
if not scheme: if not scheme:
logger.debug('_create_deep_link: no urlScheme for app', get=app.get('name', 'unknown')) logger.debug(f"_create_deep_link: no urlScheme for app '{app.get('name', 'unknown')}'")
return None return None
# Pick the correct payload based on which template the app uses # Pick the correct payload based on which template the app uses
if uses_crypto: if uses_crypto:
if not subscription_crypto_link: if not subscription_crypto_link:
logger.debug( logger.debug(
'_create_deep_link: app requires crypto link but none available', get=app.get('name', 'unknown') f"_create_deep_link: app '{app.get('name', 'unknown')}' requires crypto link but none available"
) )
return None return None
payload = subscription_crypto_link payload = subscription_crypto_link
else: else:
if not subscription_url: if not subscription_url:
logger.debug( logger.debug(
'_create_deep_link: app requires subscription_url but none available', get=app.get('name', 'unknown') f"_create_deep_link: app '{app.get('name', 'unknown')}' requires subscription_url but none available"
) )
return None return None
payload = subscription_url payload = subscription_url
@@ -3050,7 +3024,7 @@ def _create_deep_link(
try: try:
payload = base64.b64encode(payload.encode('utf-8')).decode('utf-8') payload = base64.b64encode(payload.encode('utf-8')).decode('utf-8')
except Exception as e: except Exception as e:
logger.warning('Failed to encode payload to base64', error=e) logger.warning(f'Failed to encode payload to base64: {e}')
return f'{scheme}{payload}' return f'{scheme}{payload}'
@@ -3259,7 +3233,7 @@ async def update_countries(
try: try:
await add_user_to_servers(db, added_server_ids) await add_user_to_servers(db, added_server_ids)
except Exception as e: except Exception as e:
logger.error('Ошибка обновления счётчика серверов', error=e) logger.error(f'Ошибка обновления счётчика серверов: {e}')
# Update connected squads # Update connected squads
user.subscription.connected_squads = selected_countries user.subscription.connected_squads = selected_countries
@@ -3274,7 +3248,7 @@ async def update_countries(
else: else:
await subscription_service.create_remnawave_user(db, user.subscription) await subscription_service.create_remnawave_user(db, user.subscription)
except Exception as e: except Exception as e:
logger.error('Failed to sync countries with RemnaWave', error=e) logger.error(f'Failed to sync countries with RemnaWave: {e}')
await db.refresh(user.subscription) await db.refresh(user.subscription)
@@ -3609,7 +3583,7 @@ async def get_devices(
} }
except Exception as e: except Exception as e:
logger.error('Error fetching devices', error=e) logger.error(f'Error fetching devices: {e}')
return { return {
'devices': [], 'devices': [],
'total': 0, 'total': 0,
@@ -3653,7 +3627,7 @@ async def delete_device(
} }
except Exception as e: except Exception as e:
logger.error('Error deleting device', error=e) logger.error(f'Error deleting device: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to delete device', detail='Failed to delete device',
@@ -3712,7 +3686,7 @@ async def delete_all_devices(
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data) await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
deleted_count += 1 deleted_count += 1
except Exception as device_error: except Exception as device_error:
logger.error('Error deleting device', device_hwid=device_hwid, device_error=device_error) logger.error(f'Error deleting device {device_hwid}: {device_error}')
return { return {
'success': True, 'success': True,
@@ -3721,7 +3695,7 @@ async def delete_all_devices(
} }
except Exception as e: except Exception as e:
logger.error('Error deleting all devices', error=e) logger.error(f'Error deleting all devices: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to delete devices', detail='Failed to delete devices',
@@ -3795,7 +3769,7 @@ async def get_device_reduction_info(
if response and 'response' in response: if response and 'response' in response:
connected_devices_count = response['response'].get('total', 0) connected_devices_count = response['response'].get('total', 0)
except Exception as e: except Exception as e:
logger.error('Error getting connected devices count', error=e) logger.error(f'Error getting connected devices count: {e}')
can_reduce = current_device_limit - min_device_limit can_reduce = current_device_limit - min_device_limit
@@ -3879,11 +3853,8 @@ async def reduce_devices(
if connected_devices_count > new_device_limit: if connected_devices_count > new_device_limit:
devices_to_remove = connected_devices_count - new_device_limit devices_to_remove = connected_devices_count - new_device_limit
logger.info( logger.info(
'Removing excess devices for user had new limit', f'Removing {devices_to_remove} excess devices for user {user.id}: '
devices_to_remove=devices_to_remove, f'had {connected_devices_count}, new limit {new_device_limit}'
user_id=user.id,
connected_devices_count=connected_devices_count,
new_device_limit=new_device_limit,
) )
# Sort by date (oldest first) and remove the last ones # Sort by date (oldest first) and remove the last ones
@@ -3900,11 +3871,11 @@ async def reduce_devices(
delete_data = {'userUuid': user.remnawave_uuid, 'hwid': device_hwid} delete_data = {'userUuid': user.remnawave_uuid, 'hwid': device_hwid}
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data) await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
devices_removed_count += 1 devices_removed_count += 1
logger.info('Removed device for user', device_hwid=device_hwid, user_id=user.id) logger.info(f'Removed device {device_hwid} for user {user.id}')
except Exception as del_error: except Exception as del_error:
logger.error('Error removing device', device_hwid=device_hwid, del_error=del_error) logger.error(f'Error removing device {device_hwid}: {del_error}')
except Exception as e: except Exception as e:
logger.error('Error checking/removing devices', error=e) logger.error(f'Error checking/removing devices: {e}')
old_device_limit = current_device_limit old_device_limit = current_device_limit
@@ -3918,7 +3889,7 @@ async def reduce_devices(
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription) await subscription_service.update_remnawave_user(db, subscription)
except Exception as e: except Exception as e:
logger.error('Error updating RemnaWave user', error=e) logger.error(f'Error updating RemnaWave user: {e}')
logger.info( logger.info(
f'User {user.id} reduced device limit from {old_device_limit} to {new_device_limit}' f'User {user.id} reduced device limit from {old_device_limit} to {new_device_limit}'
@@ -4352,7 +4323,7 @@ async def switch_tariff(
else: else:
await subscription_service.create_remnawave_user(db, user.subscription) await subscription_service.create_remnawave_user(db, user.subscription)
except Exception as e: except Exception as e:
logger.error('Failed to sync tariff switch with RemnaWave', error=e) logger.error(f'Failed to sync tariff switch with RemnaWave: {e}')
# Reset all devices on tariff switch # Reset all devices on tariff switch
devices_reset = False devices_reset = False
@@ -4362,9 +4333,9 @@ async def switch_tariff(
async with service.get_api_client() as api: async with service.get_api_client() as api:
await api.reset_user_devices(user.remnawave_uuid) await api.reset_user_devices(user.remnawave_uuid)
devices_reset = True devices_reset = True
logger.info('Reset all devices for user on tariff switch', user_id=user.id) logger.info(f'Reset all devices for user {user.id} on tariff switch')
except Exception as e: except Exception as e:
logger.error('Failed to reset devices on tariff switch', error=e) logger.error(f'Failed to reset devices on tariff switch: {e}')
await db.refresh(user) await db.refresh(user)
await db.refresh(user.subscription) await db.refresh(user.subscription)
@@ -4392,7 +4363,7 @@ async def switch_tariff(
finally: finally:
await bot.session.close() await bot.session.close()
except Exception as e: except Exception as e:
logger.error('Failed to send admin notification for tariff switch', error=e) logger.error(f'Failed to send admin notification for tariff switch: {e}')
response = { response = {
'success': True, 'success': True,
@@ -4491,7 +4462,7 @@ async def toggle_subscription_pause(
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
await subscription_service.enable_remnawave_user(user.remnawave_uuid) await subscription_service.enable_remnawave_user(user.remnawave_uuid)
except Exception as e: except Exception as e:
logger.error('Error enabling RemnaWave user on resume', error=e) logger.error(f'Error enabling RemnaWave user on resume: {e}')
if new_paused_state: if new_paused_state:
message = 'Daily subscription paused' message = 'Daily subscription paused'
@@ -4625,7 +4596,7 @@ async def switch_traffic_package(
else: else:
await subscription_service.create_remnawave_user(db, user.subscription) await subscription_service.create_remnawave_user(db, user.subscription)
except Exception as e: except Exception as e:
logger.error('Failed to sync traffic switch with RemnaWave', error=e) logger.error(f'Failed to sync traffic switch with RemnaWave: {e}')
await db.refresh(user) await db.refresh(user)
await db.refresh(user.subscription) await db.refresh(user.subscription)
@@ -4766,7 +4737,7 @@ async def refresh_traffic(
} }
except Exception as e: except Exception as e:
logger.error('Error refreshing traffic for user', user_id=user.id, error=e) logger.error(f'Error refreshing traffic for user {user.id}: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to refresh traffic data', detail='Failed to refresh traffic data',
+2 -2
View File
@@ -1,8 +1,8 @@
"""Ticket notifications routes for cabinet.""" """Ticket notifications routes for cabinet."""
import logging
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +13,7 @@ from app.database.models import User
from ..dependencies import get_cabinet_db, get_current_admin_user, get_current_cabinet_user from ..dependencies import get_cabinet_db, get_current_admin_user, get_current_cabinet_user
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/tickets/notifications', tags=['Cabinet Ticket Notifications']) router = APIRouter(prefix='/tickets/notifications', tags=['Cabinet Ticket Notifications'])
admin_router = APIRouter(prefix='/admin/tickets/notifications', tags=['Cabinet Admin Ticket Notifications']) admin_router = APIRouter(prefix='/admin/tickets/notifications', tags=['Cabinet Admin Ticket Notifications'])
+6 -6
View File
@@ -1,9 +1,9 @@
"""Support tickets routes for cabinet.""" """Support tickets routes for cabinet."""
import logging
import math import math
from datetime import datetime from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import desc, func, select from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -26,7 +26,7 @@ from ..schemas.tickets import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/tickets', tags=['Cabinet Tickets']) router = APIRouter(prefix='/tickets', tags=['Cabinet Tickets'])
@@ -164,7 +164,7 @@ async def create_ticket(
try: try:
await notify_admins_about_new_ticket(ticket, db) await notify_admins_about_new_ticket(ticket, db)
except Exception as e: except Exception as e:
logger.error('Error notifying admins about new ticket from cabinet', error=e) logger.error(f'Error notifying admins about new ticket from cabinet: {e}')
# Уведомить админов в кабинете # Уведомить админов в кабинете
try: try:
@@ -173,7 +173,7 @@ async def create_ticket(
# Отправить WebSocket уведомление # Отправить WebSocket уведомление
await notify_admins_new_ticket(ticket.id, ticket.title, user.id) await notify_admins_new_ticket(ticket.id, ticket.title, user.id)
except Exception as e: except Exception as e:
logger.error('Error creating cabinet notification for new ticket', error=e) logger.error(f'Error creating cabinet notification for new ticket: {e}')
messages = [_message_to_response(m) for m in ticket.messages] messages = [_message_to_response(m) for m in ticket.messages]
@@ -284,7 +284,7 @@ async def add_ticket_message(
try: try:
await notify_admins_about_ticket_reply(ticket, request.message, db) await notify_admins_about_ticket_reply(ticket, request.message, db)
except Exception as e: except Exception as e:
logger.error('Error notifying admins about ticket reply from cabinet', error=e) logger.error(f'Error notifying admins about ticket reply from cabinet: {e}')
# Уведомить админов в кабинете # Уведомить админов в кабинете
try: try:
@@ -295,6 +295,6 @@ async def add_ticket_message(
# Отправить WebSocket уведомление # Отправить WebSocket уведомление
await notify_admins_ticket_reply(ticket.id, (request.message or '')[:100], user.id) await notify_admins_ticket_reply(ticket.id, (request.message or '')[:100], user.id)
except Exception as e: except Exception as e:
logger.error('Error creating cabinet notification for user reply', error=e) logger.error(f'Error creating cabinet notification for user reply: {e}')
return _message_to_response(message) return _message_to_response(message)
+18 -18
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import logging
import structlog
from fastapi import APIRouter, WebSocket, WebSocketDisconnect from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.cabinet.auth.jwt_handler import get_token_payload from app.cabinet.auth.jwt_handler import get_token_payload
@@ -14,7 +14,7 @@ from app.database.crud.user import get_user_by_id
from app.database.database import AsyncSessionLocal from app.database.database import AsyncSessionLocal
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@@ -42,10 +42,10 @@ class CabinetConnectionManager:
self._admin_connections[user_id].add(websocket) self._admin_connections[user_id].add(websocket)
logger.debug( logger.debug(
'Cabinet WS connected: user_id is_admin total_users', 'Cabinet WS connected: user_id=%d, is_admin=%s, total_users=%d',
user_id=user_id, user_id,
is_admin=is_admin, is_admin,
user_connections_count=len(self._user_connections), len(self._user_connections),
) )
async def disconnect(self, websocket: WebSocket, user_id: int) -> None: async def disconnect(self, websocket: WebSocket, user_id: int) -> None:
@@ -61,7 +61,7 @@ class CabinetConnectionManager:
if not self._admin_connections[user_id]: if not self._admin_connections[user_id]:
del self._admin_connections[user_id] del self._admin_connections[user_id]
logger.debug('Cabinet WS disconnected: user_id', user_id=user_id) logger.debug('Cabinet WS disconnected: user_id=%d', user_id)
async def send_to_user(self, user_id: int, message: dict) -> None: async def send_to_user(self, user_id: int, message: dict) -> None:
"""Отправить сообщение конкретному пользователю.""" """Отправить сообщение конкретному пользователю."""
@@ -79,7 +79,7 @@ class CabinetConnectionManager:
try: try:
await ws.send_text(data) await ws.send_text(data)
except Exception as e: except Exception as e:
logger.warning('Failed to send to user', user_id=user_id, e=e) logger.warning('Failed to send to user %d: %s', user_id, e)
disconnected.add(ws) disconnected.add(ws)
# Cleanup disconnected # Cleanup disconnected
@@ -105,7 +105,7 @@ class CabinetConnectionManager:
try: try:
await ws.send_text(data) await ws.send_text(data)
except Exception as e: except Exception as e:
logger.warning('Failed to send to admin', user_id=user_id, e=e) logger.warning('Failed to send to admin %d: %s', user_id, e)
if user_id not in disconnected_by_user: if user_id not in disconnected_by_user:
disconnected_by_user[user_id] = set() disconnected_by_user[user_id] = set()
disconnected_by_user[user_id].add(ws) disconnected_by_user[user_id].add(ws)
@@ -152,7 +152,7 @@ async def verify_cabinet_ws_token(token: str) -> tuple[int | None, bool]:
) )
return user_id, is_admin return user_id, is_admin
except (TimeoutError, OSError, ConnectionRefusedError) as e: except (TimeoutError, OSError, ConnectionRefusedError) as e:
logger.error('Database connection error in WS token verification', e=str(e)[:200]) logger.error('Database connection error in WS token verification: %s', str(e)[:200])
return None, False return None, False
@@ -165,7 +165,7 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
token = websocket.query_params.get('token') token = websocket.query_params.get('token')
if not token: if not token:
logger.debug('Cabinet WS: No token from', client_host=client_host) logger.debug('Cabinet WS: No token from %s', client_host)
# Принимаем и сразу закрываем с кодом ошибки # Принимаем и сразу закрываем с кодом ошибки
await websocket.accept() await websocket.accept()
await websocket.close(code=1008, reason='Unauthorized: No token') await websocket.close(code=1008, reason='Unauthorized: No token')
@@ -175,7 +175,7 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
user_id, is_admin = await verify_cabinet_ws_token(token) user_id, is_admin = await verify_cabinet_ws_token(token)
if not user_id: if not user_id:
logger.debug('Cabinet WS: Invalid token from', client_host=client_host) logger.debug('Cabinet WS: Invalid token from %s', client_host)
# Принимаем и сразу закрываем с кодом ошибки # Принимаем и сразу закрываем с кодом ошибки
await websocket.accept() await websocket.accept()
await websocket.close(code=1008, reason='Unauthorized: Invalid token') await websocket.close(code=1008, reason='Unauthorized: Invalid token')
@@ -184,9 +184,9 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
# Принимаем соединение # Принимаем соединение
try: try:
await websocket.accept() await websocket.accept()
logger.debug('Cabinet WS accepted: user_id is_admin', user_id=user_id, is_admin=is_admin) logger.debug('Cabinet WS accepted: user_id=%d, is_admin=%s', user_id, is_admin)
except Exception as e: except Exception as e:
logger.error('Cabinet WS: Failed to accept from', client_host=client_host, e=e) logger.error('Cabinet WS: Failed to accept from %s: %s', client_host, e)
return return
# Регистрируем подключение # Регистрируем подключение
@@ -213,17 +213,17 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
await websocket.send_json({'type': 'pong'}) await websocket.send_json({'type': 'pong'})
except json.JSONDecodeError: except json.JSONDecodeError:
logger.warning('Cabinet WS: Invalid JSON from user', user_id=user_id) logger.warning('Cabinet WS: Invalid JSON from user %d', user_id)
except WebSocketDisconnect: except WebSocketDisconnect:
break break
except Exception as e: except Exception as e:
logger.exception('Cabinet WS error for user', user_id=user_id, e=e) logger.exception('Cabinet WS error for user %d: %s', user_id, e)
break break
except WebSocketDisconnect: except WebSocketDisconnect:
logger.debug('Cabinet WS disconnected: user_id', user_id=user_id) logger.debug('Cabinet WS disconnected: user_id=%d', user_id)
except Exception as e: except Exception as e:
logger.exception('Cabinet WS error', e=e) logger.exception('Cabinet WS error: %s', e)
finally: finally:
await cabinet_ws_manager.disconnect(websocket, user_id) await cabinet_ws_manager.disconnect(websocket, user_id)
+5 -7
View File
@@ -2,11 +2,11 @@
API роуты колеса удачи для пользователей. API роуты колеса удачи для пользователей.
""" """
import logging
import math import math
import time import time
import httpx import httpx
import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -32,7 +32,7 @@ from app.database.models import User
from app.services.wheel_service import wheel_service from app.services.wheel_service import wheel_service
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix='/wheel', tags=['Fortune Wheel']) router = APIRouter(prefix='/wheel', tags=['Fortune Wheel'])
@@ -253,16 +253,14 @@ async def create_stars_invoice(
result = response.json() result = response.json()
if not result.get('ok'): if not result.get('ok'):
logger.error('Telegram API error', result=result) logger.error(f'Telegram API error: {result}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Ошибка создания инвойса', detail='Ошибка создания инвойса',
) )
invoice_url = result['result'] invoice_url = result['result']
logger.info( logger.info(f'Created Stars invoice for wheel spin: user={user.id}, stars={stars_amount}')
'Created Stars invoice for wheel spin: user=, stars', user_id=user.id, stars_amount=stars_amount
)
return StarsInvoiceResponse( return StarsInvoiceResponse(
invoice_url=invoice_url, invoice_url=invoice_url,
@@ -270,7 +268,7 @@ async def create_stars_invoice(
) )
except httpx.HTTPError as e: except httpx.HTTPError as e:
logger.error('HTTP error creating invoice', error=e) logger.error(f'HTTP error creating invoice: {e}')
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Ошибка соединения с Telegram', detail='Ошибка соединения с Telegram',
+5 -6
View File
@@ -1,15 +1,14 @@
"""Email service for sending verification and password reset emails.""" """Email service for sending verification and password reset emails."""
import logging
import smtplib import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
import structlog
from app.config import settings from app.config import settings
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class EmailService: class EmailService:
@@ -42,7 +41,7 @@ class EmailService:
if smtp.has_extn('auth'): if smtp.has_extn('auth'):
smtp.login(self.user, self.password) smtp.login(self.user, self.password)
else: else:
logger.debug('SMTP server does not support AUTH, skipping authentication', host=self.host) logger.debug(f'SMTP server {self.host} does not support AUTH, skipping authentication')
return smtp return smtp
@@ -95,11 +94,11 @@ class EmailService:
with self._get_smtp_connection() as smtp: with self._get_smtp_connection() as smtp:
smtp.sendmail(self.from_email, to_email, msg.as_string()) smtp.sendmail(self.from_email, to_email, msg.as_string())
logger.info('Email sent successfully to', to_email=to_email) logger.info(f'Email sent successfully to {to_email}')
return True return True
except Exception as e: except Exception as e:
logger.error('Failed to send email to', to_email=to_email, error=e) logger.error(f'Failed to send email to {to_email}: {e}')
return False return False
def send_verification_email( def send_verification_email(
@@ -4,17 +4,17 @@ Service for managing email template overrides stored in the database.
Custom templates override the hardcoded defaults from email_templates.py. Custom templates override the hardcoded defaults from email_templates.py.
""" """
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.database import AsyncSessionLocal from app.database.database import AsyncSessionLocal
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_template_override( async def get_template_override(
@@ -56,9 +56,7 @@ async def get_template_override(
return None return None
except Exception as e: except Exception as e:
logger.debug( logger.debug('Не удалось получить override шаблона %s/%s: %s', notification_type, language, e)
'Не удалось получить override шаблона /', notification_type=notification_type, language=language, e=e
)
return None return None
+33 -16
View File
@@ -1,6 +1,7 @@
import hashlib import hashlib
import hmac import hmac
import html import html
import logging
import math import math
import os import os
import re import re
@@ -10,7 +11,6 @@ from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import structlog
from pydantic import Field, field_validator from pydantic import Field, field_validator
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@@ -23,7 +23,7 @@ DEFAULT_DISPLAY_NAME_BANNED_KEYWORDS: list[str] = [
USER_TAG_PATTERN = re.compile(r'^[A-Z0-9_]{1,16}$') USER_TAG_PATTERN = re.compile(r'^[A-Z0-9_]{1,16}$')
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -1297,7 +1297,7 @@ class Settings(BaseSettings):
raise ValueError raise ValueError
return time(hour=hours, minute=minutes) return time(hour=hours, minute=minutes)
except (ValueError, AttributeError): except (ValueError, AttributeError):
logger.warning('Некорректное значение ADMIN_REPORTS_SEND_TIME', send_time_value=value) logging.getLogger(__name__).warning('Некорректное значение ADMIN_REPORTS_SEND_TIME: %s', value)
return None return None
def kopeks_to_rubles(self, kopeks: int) -> float: def kopeks_to_rubles(self, kopeks: int) -> float:
@@ -1317,14 +1317,17 @@ class Settings(BaseSettings):
if len(cleaned) > 16: if len(cleaned) > 16:
logger.warning( logger.warning(
'Некорректная длина : максимум 16 символов, получено', 'Некорректная длина %s: максимум 16 символов, получено %s',
setting_name=setting_name, setting_name,
cleaned_count=len(cleaned), len(cleaned),
) )
return None return None
if not USER_TAG_PATTERN.fullmatch(cleaned): if not USER_TAG_PATTERN.fullmatch(cleaned):
logger.warning('Некорректный формат : допустимы только A-Z, 0-9 и подчёркивание', setting_name=setting_name) logger.warning(
'Некорректный формат %s: допустимы только A-Z, 0-9 и подчёркивание',
setting_name,
)
return None return None
return cleaned return cleaned
@@ -1445,9 +1448,9 @@ class Settings(BaseSettings):
try: try:
return int(self.EXTERNAL_ADMIN_TOKEN_BOT_ID) if self.EXTERNAL_ADMIN_TOKEN_BOT_ID else None return int(self.EXTERNAL_ADMIN_TOKEN_BOT_ID) if self.EXTERNAL_ADMIN_TOKEN_BOT_ID else None
except (TypeError, ValueError): # pragma: no cover - защитная ветка для некорректных значений except (TypeError, ValueError): # pragma: no cover - защитная ветка для некорректных значений
logger.warning( logging.getLogger(__name__).warning(
'Некорректный идентификатор бота для внешней админки', 'Некорректный идентификатор бота для внешней админки: %s',
EXTERNAL_ADMIN_TOKEN_BOT_ID=self.EXTERNAL_ADMIN_TOKEN_BOT_ID, self.EXTERNAL_ADMIN_TOKEN_BOT_ID,
) )
return None return None
@@ -1528,7 +1531,10 @@ class Settings(BaseSettings):
try: try:
value = int(raw_value) value = int(raw_value)
except (TypeError, ValueError): except (TypeError, ValueError):
logger.warning('Некорректное значение DEVICES_SELECTION_DISABLED_AMOUNT', raw_value=raw_value) logger.warning(
'Некорректное значение DEVICES_SELECTION_DISABLED_AMOUNT: %s',
raw_value,
)
return None return None
if value < 0: if value < 0:
@@ -1568,7 +1574,8 @@ class Settings(BaseSettings):
value = int(self.TRIAL_ACTIVATION_PRICE) value = int(self.TRIAL_ACTIVATION_PRICE)
except (TypeError, ValueError): except (TypeError, ValueError):
logger.warning( logger.warning(
'Некорректное значение TRIAL_ACTIVATION_PRICE', TRIAL_ACTIVATION_PRICE=self.TRIAL_ACTIVATION_PRICE 'Некорректное значение TRIAL_ACTIVATION_PRICE: %s',
self.TRIAL_ACTIVATION_PRICE,
) )
return 0 return 0
@@ -1687,7 +1694,7 @@ class Settings(BaseSettings):
try: try:
method_code = int(part) method_code = int(part)
except ValueError: except ValueError:
logger.warning('Некорректный код метода Platega', part=part) logger.warning('Некорректный код метода Platega: %s', part)
continue continue
if method_code in {2, 10, 11, 12, 13} and method_code not in seen: if method_code in {2, 10, 11, 12, 13} and method_code not in seen:
methods.append(method_code) methods.append(method_code)
@@ -1782,8 +1789,8 @@ class Settings(BaseSettings):
if minutes <= 0: if minutes <= 0:
logger.warning( logger.warning(
'Некорректный интервал автопроверки платежей: . Используется значение по умолчанию 10 минут.', 'Некорректный интервал автопроверки платежей: %s. Используется значение по умолчанию 10 минут.',
PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES=self.PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES, self.PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES,
) )
return 10 return 10
@@ -2137,13 +2144,22 @@ class Settings(BaseSettings):
return self.REFERRAL_NOTIFICATIONS_ENABLED return self.REFERRAL_NOTIFICATIONS_ENABLED
def get_traffic_packages(self) -> list[dict]: def get_traffic_packages(self) -> list[dict]:
import logging
logger = logging.getLogger(__name__)
try: try:
packages = [] packages = []
config_str = self.TRAFFIC_PACKAGES_CONFIG.strip() config_str = self.TRAFFIC_PACKAGES_CONFIG.strip()
logger.debug(f"CONFIG STRING: '{config_str}'")
if not config_str: if not config_str:
logger.debug('CONFIG EMPTY, USING FALLBACK')
return self._get_fallback_traffic_packages() return self._get_fallback_traffic_packages()
logger.debug('PARSING CONFIG...')
for package_config in config_str.split(','): for package_config in config_str.split(','):
package_config = package_config.strip() package_config = package_config.strip()
if not package_config: if not package_config:
@@ -2162,10 +2178,11 @@ class Settings(BaseSettings):
except ValueError: except ValueError:
continue continue
logger.debug(f'PARSED {len(packages)} packages from config')
return packages if packages else self._get_fallback_traffic_packages() return packages if packages else self._get_fallback_traffic_packages()
except Exception as e: except Exception as e:
logger.warning('ERROR PARSING CONFIG', error=e) logger.info(f'ERROR PARSING CONFIG: {e}')
return self._get_fallback_traffic_packages() return self._get_fallback_traffic_packages()
def is_version_check_enabled(self) -> bool: def is_version_check_enabled(self) -> bool:
+9 -9
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import and_, delete, func, select, update from sqlalchemy import and_, delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -17,7 +17,7 @@ from app.database.models import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_campaign( async def create_campaign(
@@ -57,10 +57,10 @@ async def create_campaign(
await db.refresh(campaign) await db.refresh(campaign)
logger.info( logger.info(
'📣 Создана рекламная кампания (start bonus=)', '📣 Создана рекламная кампания %s (start=%s, bonus=%s)',
campaign_name=campaign.name, campaign.name,
start_parameter=campaign.start_parameter, campaign.start_parameter,
bonus_type=campaign.bonus_type, campaign.bonus_type,
) )
return campaign return campaign
@@ -157,14 +157,14 @@ async def update_campaign(
await db.commit() await db.commit()
await db.refresh(campaign) await db.refresh(campaign)
logger.info('✏️ Обновлена рекламная кампания', campaign_name=campaign.name, update_data=update_data) logger.info('✏️ Обновлена рекламная кампания %s (%s)', campaign.name, update_data)
return campaign return campaign
async def delete_campaign(db: AsyncSession, campaign: AdvertisingCampaign) -> bool: async def delete_campaign(db: AsyncSession, campaign: AdvertisingCampaign) -> bool:
await db.execute(delete(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id)) await db.execute(delete(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id))
await db.commit() await db.commit()
logger.info('🗑️ Удалена рекламная кампания', campaign_name=campaign.name) logger.info('🗑️ Удалена рекламная кампания %s', campaign.name)
return True return True
@@ -217,7 +217,7 @@ async def record_campaign_registration(
await db.commit() await db.commit()
await db.refresh(registration) await db.refresh(registration)
logger.info('📈 Регистрируем пользователя в кампании', user_id=user_id, campaign_id=campaign_id) logger.info('📈 Регистрируем пользователя %s в кампании %s', user_id, campaign_id)
return registration return registration
+9 -7
View File
@@ -2,17 +2,17 @@
from __future__ import annotations from __future__ import annotations
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import CloudPaymentsPayment from app.database.models import CloudPaymentsPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_cloudpayments_payment( async def create_cloudpayments_payment(
@@ -65,10 +65,10 @@ async def create_cloudpayments_payment(
await db.refresh(payment) await db.refresh(payment)
logger.debug( logger.debug(
'Created CloudPayments payment: id invoice amount', 'Created CloudPayments payment: id=%s, invoice=%s, amount=%s',
payment_id=payment.id, payment.id,
invoice_id=invoice_id, invoice_id,
amount_kopeks=amount_kopeks, amount_kopeks,
) )
return payment return payment
@@ -195,7 +195,9 @@ async def mark_cloudpayments_payment_as_paid(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Marked CloudPayments payment as paid: id invoice', payment_id=payment.id, invoice_id=payment.invoice_id 'Marked CloudPayments payment as paid: id=%s, invoice=%s',
payment.id,
payment.invoice_id,
) )
return payment return payment
+2 -2
View File
@@ -1,7 +1,7 @@
import logging
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import and_, delete, desc, select from sqlalchemy import and_, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import ContestAttempt, ContestRound, ContestTemplate, User from app.database.models import ContestAttempt, ContestRound, ContestTemplate, User
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
# Templates # Templates
+5 -11
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import and_, select from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import CryptoBotPayment from app.database.models import CryptoBotPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_cryptobot_payment( async def create_cryptobot_payment(
@@ -41,13 +41,7 @@ async def create_cryptobot_payment(
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(f'Создан CryptoBot платеж: {invoice_id} на {amount} {asset} для пользователя {user_id}')
'Создан CryptoBot платеж: на для пользователя',
invoice_id=invoice_id,
amount=amount,
asset=asset,
user_id=user_id,
)
return payment return payment
@@ -84,7 +78,7 @@ async def update_cryptobot_payment_status(
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info('Обновлен статус CryptoBot платежа', invoice_id=invoice_id, status=status) logger.info(f'Обновлен статус CryptoBot платежа {invoice_id}: {status}')
return payment return payment
@@ -102,7 +96,7 @@ async def link_cryptobot_payment_to_transaction(
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info('Связан CryptoBot платеж с транзакцией', invoice_id=invoice_id, transaction_id=transaction_id) logger.info(f'Связан CryptoBot платеж {invoice_id} с транзакцией {transaction_id}')
return payment return payment
+14 -6
View File
@@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import structlog
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -11,7 +11,7 @@ from app.database.crud.promo_offer_log import log_promo_offer_action
from app.database.models import DiscountOffer from app.database.models import DiscountOffer
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def upsert_discount_offer( async def upsert_discount_offer(
@@ -178,12 +178,17 @@ async def mark_offer_claimed(
details=details, details=details,
) )
except Exception as exc: # pragma: no cover - defensive logging except Exception as exc: # pragma: no cover - defensive logging
logger.warning('Failed to record promo offer claim log for offer', offer_id=offer.id, exc=exc) logger.warning(
'Failed to record promo offer claim log for offer %s: %s',
offer.id,
exc,
)
try: try:
await db.rollback() await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to rollback session after promo offer claim log failure', rollback_error=rollback_error 'Failed to rollback session after promo offer claim log failure: %s',
rollback_error,
) )
return offer return offer
@@ -234,13 +239,16 @@ async def deactivate_expired_offers(db: AsyncSession) -> int:
) )
except Exception as exc: # pragma: no cover - defensive logging except Exception as exc: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to record promo offer disable log for offer', payload=payload.get('offer_id'), exc=exc 'Failed to record promo offer disable log for offer %s: %s',
payload.get('offer_id'),
exc,
) )
try: try:
await db.rollback() await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to rollback session after promo offer disable log failure', rollback_error=rollback_error 'Failed to rollback session after promo offer disable log failure: %s',
rollback_error,
) )
return count return count
+5 -5
View File
@@ -1,14 +1,14 @@
import logging
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import delete, func, select, update from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import FaqPage, FaqSetting from app.database.models import FaqPage, FaqSetting
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_faq_setting(db: AsyncSession, language: str) -> FaqSetting | None: async def get_faq_setting(db: AsyncSession, language: str) -> FaqSetting | None:
@@ -94,7 +94,7 @@ async def create_faq_page(
await db.commit() await db.commit()
await db.refresh(page) await db.refresh(page)
logger.info('✅ Создана страница FAQ для языка', page_id=page.id, language=language) logger.info('✅ Создана страница FAQ %s для языка %s', page.id, language)
return page return page
@@ -122,7 +122,7 @@ async def update_faq_page(
await db.commit() await db.commit()
await db.refresh(page) await db.refresh(page)
logger.info('✅ Страница FAQ обновлена', page_id=page.id) logger.info('✅ Страница FAQ %s обновлена', page.id)
return page return page
@@ -130,7 +130,7 @@ async def update_faq_page(
async def delete_faq_page(db: AsyncSession, page_id: int) -> None: async def delete_faq_page(db: AsyncSession, page_id: int) -> None:
await db.execute(delete(FaqPage).where(FaqPage.id == page_id)) await db.execute(delete(FaqPage).where(FaqPage.id == page_id))
await db.commit() await db.commit()
logger.info('🗑️ Страница FAQ удалена', page_id=page_id) logger.info('🗑️ Страница FAQ %s удалена', page_id)
async def bulk_update_order( async def bulk_update_order(
+4 -9
View File
@@ -1,16 +1,16 @@
"""CRUD операции для платежей Freekassa.""" """CRUD операции для платежей Freekassa."""
import json import json
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import FreekassaPayment from app.database.models import FreekassaPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_freekassa_payment( async def create_freekassa_payment(
@@ -41,7 +41,7 @@ async def create_freekassa_payment(
db.add(payment) db.add(payment)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info('Создан платеж Freekassa: order_id=, user_id', order_id=order_id, user_id=user_id) logger.info(f'Создан платеж Freekassa: order_id={order_id}, user_id={user_id}')
return payment return payment
@@ -92,12 +92,7 @@ async def update_freekassa_payment_status(
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(f'Обновлен статус платежа Freekassa: order_id={payment.order_id}, status={status}, is_paid={is_paid}')
'Обновлен статус платежа Freekassa: order_id=, status=, is_paid',
order_id=payment.order_id,
status=status,
is_paid=is_paid,
)
return payment return payment
+20 -16
View File
@@ -1,7 +1,7 @@
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import HeleketPayment from app.database.models import HeleketPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_heleket_payment( async def create_heleket_payment(
@@ -50,12 +50,12 @@ async def create_heleket_payment(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Создан Heleket платеж: uuid= order_id= amount= для пользователя', 'Создан Heleket платеж: uuid=%s order_id=%s amount=%s %s для пользователя %s',
uuid=uuid, uuid,
order_id=order_id, order_id,
amount=amount, amount,
currency=currency, currency,
user_id=user_id, user_id,
) )
return payment return payment
@@ -107,7 +107,7 @@ async def update_heleket_payment(
payment = await get_heleket_payment_by_uuid(db, uuid) payment = await get_heleket_payment_by_uuid(db, uuid)
if not payment: if not payment:
logger.error('Heleket платеж с uuid= не найден', uuid=uuid) logger.error('Heleket платеж с uuid=%s не найден', uuid)
return None return None
if status is not None: if status is not None:
@@ -135,11 +135,11 @@ async def update_heleket_payment(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Обновлен Heleket платеж : статус= payer_amount', 'Обновлен Heleket платеж %s: статус=%s payer_amount=%s %s',
uuid=uuid, uuid,
payment_status=payment.status, payment.status,
payer_amount=payment.payer_amount, payment.payer_amount,
payer_currency=payment.payer_currency, payment.payer_currency,
) )
return payment return payment
@@ -153,7 +153,7 @@ async def link_heleket_payment_to_transaction(
payment = await get_heleket_payment_by_uuid(db, uuid) payment = await get_heleket_payment_by_uuid(db, uuid)
if not payment: if not payment:
logger.error('Не найден Heleket платеж для связи с транзакцией', uuid=uuid) logger.error('Не найден Heleket платеж для связи с транзакцией: %s', uuid)
return None return None
payment.transaction_id = transaction_id payment.transaction_id = transaction_id
@@ -162,6 +162,10 @@ async def link_heleket_payment_to_transaction(
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info('Heleket платеж связан с транзакцией', uuid=uuid, transaction_id=transaction_id) logger.info(
'Heleket платеж %s связан с транзакцией %s',
uuid,
transaction_id,
)
return payment return payment
+4 -9
View File
@@ -1,16 +1,16 @@
"""CRUD операции для платежей KassaAI.""" """CRUD операции для платежей KassaAI."""
import json import json
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import KassaAiPayment from app.database.models import KassaAiPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_kassa_ai_payment( async def create_kassa_ai_payment(
@@ -43,7 +43,7 @@ async def create_kassa_ai_payment(
db.add(payment) db.add(payment)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info('Создан платеж KassaAI: order_id=, user_id', order_id=order_id, user_id=user_id) logger.info(f'Создан платеж KassaAI: order_id={order_id}, user_id={user_id}')
return payment return payment
@@ -94,12 +94,7 @@ async def update_kassa_ai_payment_status(
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(f'Обновлен статус платежа KassaAI: order_id={payment.order_id}, status={status}, is_paid={is_paid}')
'Обновлен статус платежа KassaAI: order_id=, status=, is_paid',
order_id=payment.order_id,
status=status,
is_paid=is_paid,
)
return payment return payment
+8 -8
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,7 +8,7 @@ from app.config import settings
from app.database.models import MulenPayPayment from app.database.models import MulenPayPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_mulenpay_payment( async def create_mulenpay_payment(
@@ -41,12 +41,12 @@ async def create_mulenpay_payment(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Создан платеж # (uuid=) на сумму копеек для пользователя', 'Создан %s платеж #%s (uuid=%s) на сумму %s копеек для пользователя %s',
get_mulenpay_display_name=settings.get_mulenpay_display_name(), settings.get_mulenpay_display_name(),
mulen_payment_id=payment.mulen_payment_id, payment.mulen_payment_id,
uuid=uuid, uuid,
amount_kopeks=amount_kopeks, amount_kopeks,
user_id=user_id, user_id,
) )
return payment return payment
+3 -2
View File
@@ -1,11 +1,12 @@
import structlog import logging
from sqlalchemy import delete, select from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import SentNotification from app.database.models import SentNotification
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def notification_sent( async def notification_sent(
+16 -12
View File
@@ -2,17 +2,17 @@
from __future__ import annotations from __future__ import annotations
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import Pal24Payment from app.database.models import Pal24Payment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_pal24_payment( async def create_pal24_payment(
@@ -51,11 +51,11 @@ async def create_pal24_payment(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Создан Pal24 платеж # для пользователя : копеек (статус)', 'Создан Pal24 платеж #%s для пользователя %s: %s копеек (статус %s)',
payment_id=payment.id, payment.id,
user_id=user_id, user_id,
amount_kopeks=amount_kopeks, amount_kopeks,
status=status, status,
) )
return payment return payment
@@ -128,10 +128,10 @@ async def update_pal24_payment_status(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Обновлен Pal24 платеж : статус is_paid', 'Обновлен Pal24 платеж %s: статус=%s, is_paid=%s',
bill_id=payment.bill_id, payment.bill_id,
payment_status=payment.status, payment.status,
is_paid=payment.is_paid, payment.is_paid,
) )
return payment return payment
@@ -145,5 +145,9 @@ async def link_pal24_payment_to_transaction(
await db.execute(update(Pal24Payment).where(Pal24Payment.id == payment.id).values(transaction_id=transaction_id)) await db.execute(update(Pal24Payment).where(Pal24Payment.id == payment.id).values(transaction_id=transaction_id))
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
logger.info('Pal24 платеж привязан к транзакции', bill_id=payment.bill_id, transaction_id=transaction_id) logger.info(
'Pal24 платеж %s привязан к транзакции %s',
payment.bill_id,
transaction_id,
)
return payment return payment
+7 -7
View File
@@ -2,17 +2,17 @@
from __future__ import annotations from __future__ import annotations
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PlategaPayment from app.database.models import PlategaPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_platega_payment( async def create_platega_payment(
@@ -55,11 +55,11 @@ async def create_platega_payment(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Создан Platega платеж # (tx=) на сумму копеек для пользователя', 'Создан Platega платеж #%s (tx=%s) на сумму %s копеек для пользователя %s',
payment_id=payment.id, payment.id,
platega_transaction_id=platega_transaction_id, platega_transaction_id,
amount_kopeks=amount_kopeks, amount_kopeks,
user_id=user_id, user_id,
) )
return payment return payment
+3 -3
View File
@@ -1,6 +1,6 @@
import logging
from collections.abc import Iterable, Sequence from collections.abc import Iterable, Sequence
import structlog
from sqlalchemy import and_, delete, func, select from sqlalchemy import and_, delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -14,7 +14,7 @@ from app.database.models import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_poll( async def create_poll(
@@ -97,7 +97,7 @@ async def delete_poll(db: AsyncSession, poll_id: int) -> bool:
await db.delete(poll) await db.delete(poll)
await db.commit() await db.commit()
logger.info('🗑️ Удалён опрос', poll_id=poll_id) logger.info('🗑️ Удалён опрос %s', poll_id)
return True return True
+7 -3
View File
@@ -1,13 +1,13 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PrivacyPolicy from app.database.models import PrivacyPolicy
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_privacy_policy(db: AsyncSession, language: str) -> PrivacyPolicy | None: async def get_privacy_policy(db: AsyncSession, language: str) -> PrivacyPolicy | None:
@@ -38,7 +38,11 @@ async def upsert_privacy_policy(
await db.commit() await db.commit()
await db.refresh(policy) await db.refresh(policy)
logger.info('✅ Политика конфиденциальности для языка обновлена (ID:)', language=language, policy_id=policy.id) logger.info(
'✅ Политика конфиденциальности для языка %s обновлена (ID: %s)',
language,
policy.id,
)
return policy return policy
+12 -7
View File
@@ -1,4 +1,5 @@
import structlog import logging
from sqlalchemy import func, select, update from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -24,7 +25,7 @@ def _normalize_period_discounts(period_discounts: dict[int, int] | None) -> dict
return normalized return normalized
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_promo_groups_with_counts( async def get_promo_groups_with_counts(
@@ -193,7 +194,11 @@ async def update_promo_group(
await db.commit() await db.commit()
await db.refresh(group) await db.refresh(group)
logger.info("Обновлена промогруппа '' (id=)", group_name=group.name, group_id=group.id) logger.info(
"Обновлена промогруппа '%s' (id=%s)",
group.name,
group.id,
)
return group return group
@@ -241,10 +246,10 @@ async def delete_promo_group(db: AsyncSession, group: PromoGroup) -> bool:
await db.commit() await db.commit()
logger.info( logger.info(
"Промогруппа '' (id=) удалена, пользователи переведены в ''", "Промогруппа '%s' (id=%s) удалена, пользователи переведены в '%s'",
group_name=group.name, group.name,
group_id=group.id, group.id,
default_group_name=default_group.name, default_group.name,
) )
return True return True
+3 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import structlog import logging
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -8,7 +9,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoOfferLog from app.database.models import PromoOfferLog
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def log_promo_offer_action( async def log_promo_offer_action(
+9 -9
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoCode, PromoCodeType, PromoCodeUse, User from app.database.models import PromoCode, PromoCodeType, PromoCodeUse, User
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_promocode_by_code(db: AsyncSession, code: str) -> PromoCode | None: async def get_promocode_by_code(db: AsyncSession, code: str) -> PromoCode | None:
@@ -78,9 +78,9 @@ async def create_promocode(
await db.refresh(promocode) await db.refresh(promocode)
if promo_group_id: if promo_group_id:
logger.info('✅ Создан промокод: с промогруппой ID', code=code, promo_group_id=promo_group_id) logger.info(f'✅ Создан промокод: {code} с промогруппой ID {promo_group_id}')
else: else:
logger.info('✅ Создан промокод', code=code) logger.info(f'✅ Создан промокод: {code}')
return promocode return promocode
@@ -97,11 +97,11 @@ async def use_promocode(db: AsyncSession, promocode_id: int, user_id: int) -> bo
await db.commit() await db.commit()
logger.info('✅ Промокод использован пользователем', code=promocode.code, user_id=user_id) logger.info(f'✅ Промокод {promocode.code} использован пользователем {user_id}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка использования промокода', error=e) logger.error(f'Ошибка использования промокода: {e}')
await db.rollback() await db.rollback()
return False return False
@@ -120,7 +120,7 @@ async def create_promocode_use(db: AsyncSession, promocode_id: int, user_id: int
await db.commit() await db.commit()
await db.refresh(promocode_use) await db.refresh(promocode_use)
logger.info('📝 Записано использование промокода пользователем', promocode_id=promocode_id, user_id=user_id) logger.info(f'📝 Записано использование промокода {promocode_id} пользователем {user_id}')
return promocode_use return promocode_use
@@ -194,11 +194,11 @@ async def delete_promocode(db: AsyncSession, promocode: PromoCode) -> bool:
await db.delete(promocode) await db.delete(promocode)
await db.commit() await db.commit()
logger.info('🗑️ Удален промокод', code=promocode.code) logger.info(f'🗑️ Удален промокод: {promocode.code}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка удаления промокода', error=e) logger.error(f'Ошибка удаления промокода: {e}')
await db.rollback() await db.rollback()
return False return False
+7 -3
View File
@@ -1,13 +1,13 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PublicOffer from app.database.models import PublicOffer
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_public_offer(db: AsyncSession, language: str) -> PublicOffer | None: async def get_public_offer(db: AsyncSession, language: str) -> PublicOffer | None:
@@ -38,7 +38,11 @@ async def upsert_public_offer(
await db.commit() await db.commit()
await db.refresh(offer) await db.refresh(offer)
logger.info('✅ Публичная оферта для языка обновлена (ID:)', language=language, offer_id=offer.id) logger.info(
'✅ Публичная оферта для языка %s обновлена (ID: %s)',
language,
offer.id,
)
return offer return offer
+4 -9
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import structlog
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import ReferralEarning, User from app.database.models import ReferralEarning, User
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_referral_earning( async def create_referral_earning(
@@ -31,9 +31,7 @@ async def create_referral_earning(
await db.commit() await db.commit()
await db.refresh(earning) await db.refresh(earning)
logger.info( logger.info(f'💰 Создан реферальный заработок: {amount_kopeks / 100}₽ для пользователя {user_id}')
'💰 Создан реферальный заработок: ₽ для пользователя', amount_kopeks=amount_kopeks / 100, user_id=user_id
)
return earning return earning
@@ -213,10 +211,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
month_earnings = month_referral_earnings_result.scalar() + month_transaction_earnings_result.scalar() month_earnings = month_referral_earnings_result.scalar() + month_transaction_earnings_result.scalar()
logger.info( logger.info(
'Реферальная статистика: рефералов, рефереров, выплачено копеек', f'Реферальная статистика: {users_with_referrals} рефералов, {active_referrers} рефереров, выплачено {total_paid} копеек'
users_with_referrals=users_with_referrals,
active_referrers=active_referrers,
total_paid=total_paid,
) )
return { return {
+15 -26
View File
@@ -1,7 +1,7 @@
import logging
from collections.abc import Sequence from collections.abc import Sequence
from datetime import date, datetime, time from datetime import date, datetime, time
import structlog
from sqlalchemy import and_, desc, func, select from sqlalchemy import and_, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -16,7 +16,7 @@ from app.database.models import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_referral_contest( async def create_referral_contest(
@@ -725,12 +725,7 @@ async def sync_contest_events(
# Конец дня: 23:59:59.999999 # Конец дня: 23:59:59.999999
contest_end = contest_end.replace(hour=23, minute=59, second=59, microsecond=999999) contest_end = contest_end.replace(hour=23, minute=59, second=59, microsecond=999999)
logger.info( logger.info('Синхронизация конкурса %s: период с %s по %s', contest_id, contest_start, contest_end)
'Синхронизация конкурса : период с по',
contest_id=contest_id,
contest_start=contest_start,
contest_end=contest_end,
)
stats = { stats = {
'updated': 0, 'updated': 0,
@@ -820,11 +815,7 @@ async def sync_contest_events(
# Логируем значительные изменения # Логируем значительные изменения
if abs(old_amount - total_paid) > 10000: # больше 100 руб разницы if abs(old_amount - total_paid) > 10000: # больше 100 руб разницы
logger.debug( logger.debug(
'Событие (реферал): -> коп.', 'Событие %s (реферал %s): %s -> %s коп.', event.id, event.referral_id, old_amount, total_paid
event_id=event.id,
referral_id=event.referral_id,
old_amount=old_amount,
total_paid=total_paid,
) )
else: else:
stats['skipped'] += 1 stats['skipped'] += 1
@@ -833,11 +824,11 @@ async def sync_contest_events(
await db.commit() await db.commit()
logger.info( logger.info(
'Синхронизация конкурса завершена: обновлено , пропущено , сумма коп.', 'Синхронизация конкурса %s завершена: обновлено %s, пропущено %s, сумма %s коп.',
contest_id=contest_id, contest_id,
stats=stats['updated'], stats['updated'],
stats_2=stats['skipped'], stats['skipped'],
stats_3=stats['total_amount'], stats['total_amount'],
) )
return stats return stats
@@ -870,9 +861,7 @@ async def cleanup_invalid_contest_events(
if contest_end.hour == 0 and contest_end.minute == 0 and contest_end.second == 0: if contest_end.hour == 0 and contest_end.minute == 0 and contest_end.second == 0:
contest_end = contest_end.replace(hour=23, minute=59, second=59, microsecond=999999) contest_end = contest_end.replace(hour=23, minute=59, second=59, microsecond=999999)
logger.info( logger.info('Очистка конкурса %s: период с %s по %s', contest_id, contest_start, contest_end)
'Очистка конкурса : период с по', contest_id=contest_id, contest_start=contest_start, contest_end=contest_end
)
# Считаем сколько было событий до очистки # Считаем сколько было событий до очистки
total_before_result = await db.execute( total_before_result = await db.execute(
@@ -916,11 +905,11 @@ async def cleanup_invalid_contest_events(
remaining = int(remaining_result.scalar_one() or 0) remaining = int(remaining_result.scalar_one() or 0)
logger.info( logger.info(
'Очистка конкурса завершена: удалено невалидных событий, осталось валидных (было)', 'Очистка конкурса %s завершена: удалено %s невалидных событий, осталось %s валидных (было %s)',
contest_id=contest_id, contest_id,
deleted=deleted, deleted,
remaining=remaining, remaining,
total_before=total_before, total_before,
) )
return { return {
+9 -13
View File
@@ -1,13 +1,13 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import ServiceRule from app.database.models import ServiceRule
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_rules_by_language(db: AsyncSession, language: str = 'ru') -> ServiceRule | None: async def get_rules_by_language(db: AsyncSession, language: str = 'ru') -> ServiceRule | None:
@@ -38,7 +38,7 @@ async def create_or_update_rules(
await db.commit() await db.commit()
await db.refresh(new_rules) await db.refresh(new_rules)
logger.info('✅ Правила для языка обновлены (ID: )', language=language, new_rules_id=new_rules.id) logger.info(f'✅ Правила для языка {language} обновлены (ID: {new_rules.id})')
return new_rules return new_rules
@@ -53,14 +53,12 @@ async def clear_all_rules(db: AsyncSession, language: str = 'ru') -> bool:
await db.commit() await db.commit()
rows_affected = result.rowcount rows_affected = result.rowcount
logger.info( logger.info(f'✅ Очищены правила для языка {language}. Деактивировано записей: {rows_affected}')
'✅ Очищены правила для языка . Деактивировано записей', language=language, rows_affected=rows_affected
)
return rows_affected > 0 return rows_affected > 0
except Exception as e: except Exception as e:
logger.error('❌ Ошибка при очистке правил для языка', language=language, error=e) logger.error(f'❌ Ошибка при очистке правил для языка {language}: {e}')
await db.rollback() await db.rollback()
raise raise
@@ -104,7 +102,7 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
rule_to_restore = result.scalar_one_or_none() rule_to_restore = result.scalar_one_or_none()
if not rule_to_restore: if not rule_to_restore:
logger.warning('Правило с ID не найдено для языка', rule_id=rule_id, language=language) logger.warning(f'Правило с ID {rule_id} не найдено для языка {language}')
return None return None
await db.execute( await db.execute(
@@ -121,13 +119,11 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
await db.commit() await db.commit()
await db.refresh(restored_rule) await db.refresh(restored_rule)
logger.info( logger.info(f'✅ Восстановлена версия правил ID {rule_id} как новое правило ID {restored_rule.id}')
'✅ Восстановлена версия правил ID как новое правило ID', rule_id=rule_id, restored_rule_id=restored_rule.id
)
return restored_rule return restored_rule
except Exception as e: except Exception as e:
logger.error('❌ Ошибка при восстановлении правил ID', rule_id=rule_id, error=e) logger.error(f'❌ Ошибка при восстановлении правил ID {rule_id}: {e}')
await db.rollback() await db.rollback()
raise raise
@@ -160,5 +156,5 @@ async def get_rules_statistics(db: AsyncSession) -> dict:
} }
except Exception as e: except Exception as e:
logger.error('❌ Ошибка при получении статистики правил', error=e) logger.error(f'❌ Ошибка при получении статистики правил: {e}')
return {'total_active': 0, 'total_all_time': 0, 'languages': {}, 'total_languages': 0, 'error': str(e)} return {'total_active': 0, 'total_all_time': 0, 'languages': {}, 'total_languages': 0, 'error': str(e)}
+34 -30
View File
@@ -1,8 +1,8 @@
import logging
import random import random
from collections.abc import Iterable, Sequence from collections.abc import Iterable, Sequence
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import ( from sqlalchemy import (
String, String,
and_, and_,
@@ -28,7 +28,7 @@ from app.database.models import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def _get_default_promo_group_id(db: AsyncSession) -> int | None: async def _get_default_promo_group_id(db: AsyncSession) -> int | None:
@@ -64,7 +64,7 @@ async def create_server_squad(
promo_groups = promo_groups_result.scalars().all() promo_groups = promo_groups_result.scalars().all()
if len(promo_groups) != len(normalized_group_ids): if len(promo_groups) != len(normalized_group_ids):
logger.warning('Не все промогруппы найдены при создании сервера', display_name=display_name) logger.warning('Не все промогруппы найдены при создании сервера %s', display_name)
server_squad = ServerSquad( server_squad = ServerSquad(
squad_uuid=squad_uuid, squad_uuid=squad_uuid,
@@ -84,7 +84,7 @@ async def create_server_squad(
await db.commit() await db.commit()
await db.refresh(server_squad) await db.refresh(server_squad)
logger.info('✅ Создан сервер (UUID: )', display_name=display_name, squad_uuid=squad_uuid) logger.info(f'✅ Создан сервер {display_name} (UUID: {squad_uuid})')
return server_squad return server_squad
@@ -267,17 +267,13 @@ async def delete_server_squad(db: AsyncSession, server_id: int) -> bool:
connections_count = connections_result.scalar() connections_count = connections_result.scalar()
if connections_count > 0: if connections_count > 0:
logger.warning( logger.warning(f'⚠ Нельзя удалить сервер {server_id}: есть активные подключения ({connections_count})')
'⚠ Нельзя удалить сервер есть активные подключения',
server_id=server_id,
connections_count=connections_count,
)
return False return False
await db.execute(delete(ServerSquad).where(ServerSquad.id == server_id)) await db.execute(delete(ServerSquad).where(ServerSquad.id == server_id))
await db.commit() await db.commit()
logger.info('🗑️ Удален сервер (ID: )', server_id=server_id) logger.info(f'🗑️ Удален сервер (ID: {server_id})')
return True return True
@@ -326,7 +322,11 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
subscription_ids = {row[0] for row in subscription_ids_result.fetchall()} subscription_ids = {row[0] for row in subscription_ids_result.fetchall()}
for server in removed_servers: for server in removed_servers:
logger.info('🗑️ Удаляется сервер (UUID:)', display_name=server.display_name, squad_uuid=server.squad_uuid) logger.info(
'🗑️ Удаляется сервер %s (UUID: %s)',
server.display_name,
server.squad_uuid,
)
await db.execute(delete(SubscriptionServer).where(SubscriptionServer.server_squad_id.in_(removed_ids))) await db.execute(delete(SubscriptionServer).where(SubscriptionServer.server_squad_id.in_(removed_ids)))
@@ -386,14 +386,20 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
removed = len(removed_servers) removed = len(removed_servers)
if cleaned_subscriptions: if cleaned_subscriptions:
logger.info('🧹 Обновлены подписки после удаления серверов', cleaned_subscriptions=cleaned_subscriptions) logger.info(
'🧹 Обновлены подписки после удаления серверов: %s',
cleaned_subscriptions,
)
if cleaned_tariffs: if cleaned_tariffs:
logger.info('🧹 Обновлены тарифы после удаления серверов', cleaned_tariffs=cleaned_tariffs) logger.info(
'🧹 Обновлены тарифы после удаления серверов: %s',
cleaned_tariffs,
)
await db.commit() await db.commit()
logger.info('🔄 Синхронизация завершена: + ~', created=created, updated=updated, removed=removed) logger.info(f'🔄 Синхронизация завершена: +{created} ~{updated} -{removed}')
return created, updated, removed return created, updated, removed
@@ -761,11 +767,11 @@ async def add_user_to_servers(db: AsyncSession, server_squad_ids: list[int]) ->
) )
await db.flush() await db.flush()
logger.info('✅ Увеличен счетчик пользователей для серверов', server_squad_ids=server_squad_ids) logger.info(f'✅ Увеличен счетчик пользователей для серверов: {server_squad_ids}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка увеличения счетчика пользователей', error=e) logger.error(f'Ошибка увеличения счетчика пользователей: {e}')
raise raise
@@ -779,11 +785,11 @@ async def remove_user_from_servers(db: AsyncSession, server_squad_ids: list[int]
) )
await db.flush() await db.flush()
logger.info('✅ Уменьшен счетчик пользователей для серверов', server_squad_ids=server_squad_ids) logger.info(f'✅ Уменьшен счетчик пользователей для серверов: {server_squad_ids}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка уменьшения счетчика пользователей', error=e) logger.error(f'Ошибка уменьшения счетчика пользователей: {e}')
raise raise
@@ -830,12 +836,12 @@ async def update_server_user_counts(
await db.flush() await db.flush()
if add_set: if add_set:
logger.info('✅ Увеличен счетчик пользователей для серверов', sorted=sorted(add_set)) logger.info('✅ Увеличен счетчик пользователей для серверов: %s', sorted(add_set))
if remove_set: if remove_set:
logger.info('✅ Уменьшен счетчик пользователей для серверов', sorted=sorted(remove_set)) logger.info('✅ Уменьшен счетчик пользователей для серверов: %s', sorted(remove_set))
except Exception as e: except Exception as e:
logger.error('Ошибка обновления счетчиков серверов', e=e) logger.error('Ошибка обновления счетчиков серверов: %s', e)
raise raise
@@ -869,7 +875,7 @@ async def ensure_servers_synced(db: AsyncSession) -> None:
server_count = result.scalar() or 0 server_count = result.scalar() or 0
if server_count > 0: if server_count > 0:
logger.info('✅ В базе уже есть серверов, пропускаем синхронизацию', server_count=server_count) logger.info(f'В базе уже есть {server_count} серверов, пропускаем синхронизацию')
return return
logger.info('🔄 Серверов в БД нет, начинаем синхронизацию с RemnaWave...') logger.info('🔄 Серверов в БД нет, начинаем синхронизацию с RemnaWave...')
@@ -894,10 +900,10 @@ async def ensure_servers_synced(db: AsyncSession) -> None:
# Синхронизируем # Синхронизируем
created, updated, removed = await sync_with_remnawave(db, squads) created, updated, removed = await sync_with_remnawave(db, squads)
logger.info('✅ Серверы синхронизированы: + ~', created=created, updated=updated, removed=removed) logger.info(f'✅ Серверы синхронизированы: +{created} ~{updated} -{removed}')
except Exception as e: except Exception as e:
logger.error('❌ Ошибка синхронизации серверов', error=e) logger.error(f'❌ Ошибка синхронизации серверов: {e}')
async def sync_server_user_counts(db: AsyncSession) -> int: async def sync_server_user_counts(db: AsyncSession) -> int:
@@ -905,7 +911,7 @@ async def sync_server_user_counts(db: AsyncSession) -> int:
all_servers_result = await db.execute(select(ServerSquad.id, ServerSquad.squad_uuid)) all_servers_result = await db.execute(select(ServerSquad.id, ServerSquad.squad_uuid))
all_servers = all_servers_result.fetchall() all_servers = all_servers_result.fetchall()
logger.info('🔍 Найдено серверов для синхронизации', all_servers_count=len(all_servers)) logger.info(f'🔍 Найдено серверов для синхронизации: {len(all_servers)}')
updated_count = 0 updated_count = 0
for server_id, squad_uuid in all_servers: for server_id, squad_uuid in all_servers:
@@ -920,18 +926,16 @@ async def sync_server_user_counts(db: AsyncSession) -> int:
) )
actual_users = count_result.scalar() or 0 actual_users = count_result.scalar() or 0
logger.info( logger.info(f'📊 Сервер {server_id} ({squad_uuid[:8]}): {actual_users} пользователей')
'📊 Сервер пользователей', server_id=server_id, squad_uuid=squad_uuid[:8], actual_users=actual_users
)
await db.execute(update(ServerSquad).where(ServerSquad.id == server_id).values(current_users=actual_users)) await db.execute(update(ServerSquad).where(ServerSquad.id == server_id).values(current_users=actual_users))
updated_count += 1 updated_count += 1
await db.commit() await db.commit()
logger.info('✅ Синхронизированы счетчики для серверов', updated_count=updated_count) logger.info(f'✅ Синхронизированы счетчики для {updated_count} серверов')
return updated_count return updated_count
except Exception as e: except Exception as e:
logger.error('Ошибка синхронизации счетчиков пользователей', error=e) logger.error(f'Ошибка синхронизации счетчиков пользователей: {e}')
await db.rollback() await db.rollback()
return 0 return 0
+4 -3
View File
@@ -1,11 +1,12 @@
import structlog import logging
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import Squad from app.database.models import Squad
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def get_squad_by_uuid(db: AsyncSession, uuid: str) -> Squad | None: async def get_squad_by_uuid(db: AsyncSession, uuid: str) -> Squad | None:
@@ -27,7 +28,7 @@ async def create_squad(
await db.commit() await db.commit()
await db.refresh(squad) await db.refresh(squad)
logger.info('✅ Создан сквад', name=name) logger.info(f'✅ Создан сквад: {name}')
return squad return squad
+164 -205
View File
@@ -1,8 +1,8 @@
import logging
from collections.abc import Iterable from collections.abc import Iterable
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Optional from typing import Optional
import structlog
from sqlalchemy import and_, delete, func, select from sqlalchemy import and_, delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -22,7 +22,7 @@ from app.utils.pricing_utils import calculate_months_from_days, get_remaining_mo
from app.utils.timezone import format_local_datetime from app.utils.timezone import format_local_datetime
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
_WEBHOOK_GUARD_SECONDS = 60 _WEBHOOK_GUARD_SECONDS = 60
@@ -50,10 +50,7 @@ async def get_subscription_by_user_id(db: AsyncSession, user_id: int) -> Subscri
if subscription: if subscription:
logger.info( logger.info(
'🔍 Загружена подписка для пользователя статус', f'🔍 Загружена подписка {subscription.id} для пользователя {user_id}, статус: {subscription.status}'
subscription_id=subscription.id,
user_id=user_id,
status=subscription.status,
) )
subscription = await check_and_update_subscription_status(db, subscription) subscription = await check_and_update_subscription_status(db, subscription)
@@ -96,10 +93,16 @@ async def create_trial_subscription(
if random_squad: if random_squad:
final_squads = [random_squad] final_squads = [random_squad]
logger.debug( logger.debug(
'Выбран сквад для триальной подписки пользователя', random_squad=random_squad, user_id=user_id 'Выбран сквад %s для триальной подписки пользователя %s',
random_squad,
user_id,
) )
except Exception as error: except Exception as error:
logger.error('Не удалось получить сквад для триальной подписки пользователя', user_id=user_id, error=error) logger.error(
'Не удалось получить сквад для триальной подписки пользователя %s: %s',
user_id,
error,
)
end_date = datetime.utcnow() + timedelta(days=duration_days) end_date = datetime.utcnow() + timedelta(days=duration_days)
@@ -116,7 +119,9 @@ async def create_trial_subscription(
await db.commit() await db.commit()
await db.refresh(existing) await db.refresh(existing)
logger.info( logger.info(
'🎁 Обновлена PENDING триальная подписка для пользователя', existing_id=existing.id, user_id=user_id '🎁 Обновлена PENDING триальная подписка %s для пользователя %s',
existing.id,
user_id,
) )
return existing return existing
@@ -152,14 +157,20 @@ async def create_trial_subscription(
server_ids = await get_server_ids_by_uuids(db, final_squads) server_ids = await get_server_ids_by_uuids(db, final_squads)
if server_ids: if server_ids:
await add_user_to_servers(db, server_ids) await add_user_to_servers(db, server_ids)
logger.info('📈 Обновлен счетчик пользователей для триальных сквадов', final_squads=final_squads) logger.info(
'📈 Обновлен счетчик пользователей для триальных сквадов %s',
final_squads,
)
else: else:
logger.warning('⚠️ Не удалось найти серверы для обновления счетчика (сквады)', final_squads=final_squads) logger.warning(
'⚠️ Не удалось найти серверы для обновления счетчика (сквады %s)',
final_squads,
)
except Exception as error: except Exception as error:
logger.error( logger.error(
'⚠️ Ошибка обновления счетчика пользователей для триальных сквадов', '⚠️ Ошибка обновления счетчика пользователей для триальных сквадов %s: %s',
final_squads=final_squads, final_squads,
error=error, error,
) )
return subscription return subscription
@@ -200,10 +211,7 @@ async def create_paid_subscription(
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(
'💎 Создана платная подписка для пользователя ID: статус', f'💎 Создана платная подписка для пользователя {user_id}, ID: {subscription.id}, статус: {subscription.status}'
user_id=user_id,
subscription_id=subscription.id,
status=subscription.status,
) )
squad_uuids = list(connected_squads or []) squad_uuids = list(connected_squads or [])
@@ -218,21 +226,21 @@ async def create_paid_subscription(
if server_ids: if server_ids:
await add_user_to_servers(db, server_ids) await add_user_to_servers(db, server_ids)
logger.info( logger.info(
'📈 Обновлен счетчик пользователей для платной подписки пользователя (сквады:)', '📈 Обновлен счетчик пользователей для платной подписки пользователя %s (сквады: %s)',
user_id=user_id, user_id,
squad_uuids=squad_uuids, squad_uuids,
) )
else: else:
logger.warning( logger.warning(
'⚠️ Не удалось найти серверы для обновления счетчика платной подписки пользователя (сквады:)', '⚠️ Не удалось найти серверы для обновления счетчика платной подписки пользователя %s (сквады: %s)',
user_id=user_id, user_id,
squad_uuids=squad_uuids, squad_uuids,
) )
except Exception as error: except Exception as error:
logger.error( logger.error(
'⚠️ Ошибка обновления счетчика пользователей серверов для платной подписки пользователя', '⚠️ Ошибка обновления счетчика пользователей серверов для платной подписки пользователя %s: %s',
user_id=user_id, user_id,
error=error, error,
) )
return subscription return subscription
@@ -305,16 +313,16 @@ async def replace_subscription(
) )
logger.info( logger.info(
'♻️ Обновлены параметры подписки : удалено сквадов , добавлено', '♻️ Обновлены параметры подписки %s: удалено сквадов %s, добавлено %s',
subscription_id=subscription.id, subscription.id,
squads_to_remove_count=len(squads_to_remove), len(squads_to_remove),
squads_to_add_count=len(squads_to_add), len(squads_to_add),
) )
except Exception as error: except Exception as error:
logger.error( logger.error(
'⚠️ Ошибка обновления счетчиков серверов при замене подписки', '⚠️ Ошибка обновления счетчиков серверов при замене подписки %s: %s',
subscription_id=subscription.id, subscription.id,
error=error, error,
) )
return subscription return subscription
@@ -343,12 +351,9 @@ async def extend_subscription(
""" """
current_time = datetime.utcnow() current_time = datetime.utcnow()
logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days) logger.info(f'🔄 Продление подписки {subscription.id} на {days} дней')
logger.info( logger.info(
'📊 Текущие параметры: статус=, окончание=, тариф', f'📊 Текущие параметры: статус={subscription.status}, окончание={subscription.end_date}, тариф={subscription.tariff_id}'
status=subscription.status,
end_date=subscription.end_date,
tariff_id=subscription.tariff_id,
) )
# Определяем, происходит ли СМЕНА тарифа (а не продление того же) # Определяем, происходит ли СМЕНА тарифа (а не продление того же)
@@ -356,7 +361,7 @@ async def extend_subscription(
is_tariff_change = tariff_id is not None and (subscription.tariff_id is None or tariff_id != subscription.tariff_id) is_tariff_change = tariff_id is not None and (subscription.tariff_id is None or tariff_id != subscription.tariff_id)
if is_tariff_change: if is_tariff_change:
logger.info('🔄 Обнаружена СМЕНА тарифа: ', tariff_id=subscription.tariff_id, tariff_id_2=tariff_id) logger.info(f'🔄 Обнаружена СМЕНА тарифа: {subscription.tariff_id}{tariff_id}')
# Бонусные дни от триала - добавляются ТОЛЬКО когда подписка истекла # Бонусные дни от триала - добавляются ТОЛЬКО когда подписка истекла
# и мы начинаем отсчёт с текущей даты. НЕ начисляются при смене тарифа. # и мы начинаем отсчёт с текущей даты. НЕ начисляются при смене тарифа.
@@ -366,7 +371,9 @@ async def extend_subscription(
if days < 0: if days < 0:
subscription.end_date = subscription.end_date + timedelta(days=days) subscription.end_date = subscription.end_date + timedelta(days=days)
logger.info( logger.info(
'📅 Срок подписки уменьшен на дней, новая дата окончания', abs=abs(days), end_date=subscription.end_date '📅 Срок подписки уменьшен на %s дней, новая дата окончания: %s',
abs(days),
subscription.end_date,
) )
elif is_tariff_change: elif is_tariff_change:
# При СМЕНЕ тарифа срок начинается с текущей даты + бонус от триала # При СМЕНЕ тарифа срок начинается с текущей даты + бонус от триала
@@ -376,19 +383,19 @@ async def extend_subscription(
if remaining.total_seconds() > 0: if remaining.total_seconds() > 0:
bonus_days = max(0, remaining.days) bonus_days = max(0, remaining.days)
logger.info( logger.info(
'🎁 Обнаружен остаток триала: дней для подписки', '🎁 Обнаружен остаток триала: %s дней для подписки %s',
bonus_days=bonus_days, bonus_days,
subscription_id=subscription.id, subscription.id,
) )
total_days = days + bonus_days total_days = days + bonus_days
subscription.end_date = current_time + timedelta(days=total_days) subscription.end_date = current_time + timedelta(days=total_days)
subscription.start_date = current_time subscription.start_date = current_time
logger.info('📅 СМЕНА тарифа: срок начинается с текущей даты + дней', total_days=total_days) logger.info(f'📅 СМЕНА тарифа: срок начинается с текущей даты + {total_days} дней')
elif subscription.end_date > current_time: elif subscription.end_date > current_time:
# Подписка активна - просто добавляем дни к текущей дате окончания # Подписка активна - просто добавляем дни к текущей дате окончания
# БЕЗ бонусных дней (они уже учтены в end_date) # БЕЗ бонусных дней (они уже учтены в end_date)
subscription.end_date = subscription.end_date + timedelta(days=days) subscription.end_date = subscription.end_date + timedelta(days=days)
logger.info('📅 Подписка активна, добавляем дней к текущей дате окончания', days=days) logger.info(f'📅 Подписка активна, добавляем {days} дней к текущей дате окончания')
else: else:
# Подписка истекла - начинаем с текущей даты + бонус от триала # Подписка истекла - начинаем с текущей даты + бонус от триала
if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID: if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID:
@@ -396,19 +403,14 @@ async def extend_subscription(
pass pass
total_days = days + bonus_days total_days = days + bonus_days
subscription.end_date = current_time + timedelta(days=total_days) subscription.end_date = current_time + timedelta(days=total_days)
logger.info('📅 Подписка истекла, устанавливаем новую дату окончания на дней', total_days=total_days) logger.info(f'📅 Подписка истекла, устанавливаем новую дату окончания на {total_days} дней')
# УДАЛЕНО: Автоматическая конвертация триала по длительности # УДАЛЕНО: Автоматическая конвертация триала по длительности
# Теперь триал конвертируется ТОЛЬКО после успешного коммита продления # Теперь триал конвертируется ТОЛЬКО после успешного коммита продления
# и ТОЛЬКО вызывающей функцией (например, _auto_extend_subscription) # и ТОЛЬКО вызывающей функцией (например, _auto_extend_subscription)
# Логируем статус подписки перед проверкой # Логируем статус подписки перед проверкой
logger.info( logger.info(f'🔄 Продление подписки {subscription.id}, текущий статус: {subscription.status}, дни: {days}')
'🔄 Продление подписки текущий статус: дни',
subscription_id=subscription.id,
status=subscription.status,
days=days,
)
if days > 0 and subscription.status in ( if days > 0 and subscription.status in (
SubscriptionStatus.EXPIRED.value, SubscriptionStatus.EXPIRED.value,
@@ -417,21 +419,23 @@ async def extend_subscription(
previous_status = subscription.status previous_status = subscription.status
subscription.status = SubscriptionStatus.ACTIVE.value subscription.status = SubscriptionStatus.ACTIVE.value
logger.info( logger.info(
'🔄 Статус подписки изменён с на ACTIVE', subscription_id=subscription.id, previous_status=previous_status '🔄 Статус подписки %s изменён с %s на ACTIVE',
subscription.id,
previous_status,
) )
elif days > 0 and subscription.status == SubscriptionStatus.PENDING.value: elif days > 0 and subscription.status == SubscriptionStatus.PENDING.value:
logger.warning('⚠️ Попытка продлить PENDING подписку , дни', subscription_id=subscription.id, days=days) logger.warning('⚠️ Попытка продлить PENDING подписку %s, дни: %s', subscription.id, days)
# Обновляем параметры тарифа, если переданы # Обновляем параметры тарифа, если переданы
if tariff_id is not None: if tariff_id is not None:
old_tariff_id = subscription.tariff_id old_tariff_id = subscription.tariff_id
subscription.tariff_id = tariff_id subscription.tariff_id = tariff_id
logger.info('📦 Обновлен тариф подписки: ', old_tariff_id=old_tariff_id, tariff_id=tariff_id) logger.info(f'📦 Обновлен тариф подписки: {old_tariff_id}{tariff_id}')
# При покупке тарифа сбрасываем триальный статус # При покупке тарифа сбрасываем триальный статус
if subscription.is_trial: if subscription.is_trial:
subscription.is_trial = False subscription.is_trial = False
logger.info('🎓 Подписка конвертирована из триала в платную', subscription_id=subscription.id) logger.info(f'🎓 Подписка {subscription.id} конвертирована из триала в платную')
if traffic_limit_gb is not None: if traffic_limit_gb is not None:
old_traffic = subscription.traffic_limit_gb old_traffic = subscription.traffic_limit_gb
@@ -448,19 +452,14 @@ async def extend_subscription(
subscription.purchased_traffic_gb = 0 subscription.purchased_traffic_gb = 0
subscription.traffic_reset_at = None subscription.traffic_reset_at = None
logger.info( logger.info(
'📊 Обновлен лимит трафика: ГБ → ГБ (смена тарифа, докупки сброшены)', f'📊 Обновлен лимит трафика: {old_traffic} ГБ → {traffic_limit_gb} ГБ (смена тарифа, докупки сброшены)'
old_traffic=old_traffic,
traffic_limit_gb=traffic_limit_gb,
) )
else: else:
# При ПРОДЛЕНИИ того же тарифа — сохраняем докупленный трафик # При ПРОДЛЕНИИ того же тарифа — сохраняем докупленный трафик
purchased = subscription.purchased_traffic_gb or 0 purchased = subscription.purchased_traffic_gb or 0
subscription.traffic_limit_gb = traffic_limit_gb + purchased subscription.traffic_limit_gb = traffic_limit_gb + purchased
logger.info( logger.info(
'📊 Обновлен лимит трафика: ГБ → ГБ (докупки сохранены: ГБ)', f'📊 Обновлен лимит трафика: {old_traffic} ГБ → {traffic_limit_gb + purchased} ГБ (докупки сохранены: {purchased} ГБ)'
old_traffic=old_traffic,
traffic_limit_gb=traffic_limit_gb + purchased,
purchased=purchased,
) )
elif settings.RESET_TRAFFIC_ON_PAYMENT: elif settings.RESET_TRAFFIC_ON_PAYMENT:
subscription.traffic_used_gb = 0.0 subscription.traffic_used_gb = 0.0
@@ -476,12 +475,12 @@ async def extend_subscription(
if device_limit is not None: if device_limit is not None:
old_devices = subscription.device_limit old_devices = subscription.device_limit
subscription.device_limit = device_limit subscription.device_limit = device_limit
logger.info('📱 Обновлен лимит устройств: ', old_devices=old_devices, device_limit=device_limit) logger.info(f'📱 Обновлен лимит устройств: {old_devices}{device_limit}')
if connected_squads is not None: if connected_squads is not None:
old_squads = subscription.connected_squads old_squads = subscription.connected_squads
subscription.connected_squads = connected_squads subscription.connected_squads = connected_squads
logger.info('🌍 Обновлены сквады: ', old_squads=old_squads, connected_squads=connected_squads) logger.info(f'🌍 Обновлены сквады: {old_squads}{connected_squads}')
# Обработка daily полей при смене тарифа # Обработка daily полей при смене тарифа
if is_tariff_change and tariff_id is not None: if is_tariff_change and tariff_id is not None:
@@ -515,11 +514,7 @@ async def extend_subscription(
subscription.traffic_limit_gb = fixed_limit subscription.traffic_limit_gb = fixed_limit
subscription.purchased_traffic_gb = 0 subscription.purchased_traffic_gb = 0
subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика
logger.info( logger.info(f'🔄 Сброс трафика при продлении (fixed_with_topup): {old_limit} ГБ → {fixed_limit} ГБ')
'🔄 Сброс трафика при продлении (fixed_with_topup): ГБ → ГБ',
old_limit=old_limit,
fixed_limit=fixed_limit,
)
subscription.updated_at = current_time subscription.updated_at = current_time
@@ -527,8 +522,8 @@ async def extend_subscription(
await db.refresh(subscription) await db.refresh(subscription)
await clear_notifications(db, subscription.id) await clear_notifications(db, subscription.id)
logger.info('✅ Подписка продлена до', end_date=subscription.end_date) logger.info(f'✅ Подписка продлена до: {subscription.end_date}')
logger.info('📊 Новые параметры: статус=, окончание', status=subscription.status, end_date=subscription.end_date) logger.info(f'📊 Новые параметры: статус={subscription.status}, окончание={subscription.end_date}')
return subscription return subscription
@@ -575,10 +570,7 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription,
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(
'📈 К подписке пользователя добавлено ГБ трафика (истекает )', f'📈 К подписке пользователя {subscription.user_id} добавлено {gb} ГБ трафика (истекает {new_expires_at.strftime("%d.%m.%Y")})'
user_id=subscription.user_id,
gb=gb,
new_expires_at=new_expires_at.strftime('%d.%m.%Y'),
) )
return subscription return subscription
@@ -590,7 +582,7 @@ async def add_subscription_devices(db: AsyncSession, subscription: Subscription,
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info('📱 К подписке пользователя добавлено устройств', user_id=subscription.user_id, devices=devices) logger.info(f'📱 К подписке пользователя {subscription.user_id} добавлено {devices} устройств')
return subscription return subscription
@@ -602,7 +594,7 @@ async def add_subscription_squad(db: AsyncSession, subscription: Subscription, s
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info('🌍 К подписке пользователя добавлен сквад', user_id=subscription.user_id, squad_uuid=squad_uuid) logger.info(f'🌍 К подписке пользователя {subscription.user_id} добавлен сквад {squad_uuid}')
return subscription return subscription
@@ -617,7 +609,7 @@ async def remove_subscription_squad(db: AsyncSession, subscription: Subscription
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info('🚫 Из подписки пользователя удален сквад', user_id=subscription.user_id, squad_uuid=squad_uuid) logger.info(f'🚫 Из подписки пользователя {subscription.user_id} удален сквад {squad_uuid}')
return subscription return subscription
@@ -647,7 +639,11 @@ async def decrement_subscription_server_counts(
ids_from_links = await get_subscription_server_ids(db, sub_id) ids_from_links = await get_subscription_server_ids(db, sub_id)
server_ids.update(ids_from_links) server_ids.update(ids_from_links)
except Exception as error: except Exception as error:
logger.error('⚠️ Не удалось получить серверы подписки для уменьшения счетчика', sub_id=sub_id, error=error) logger.error(
'⚠️ Не удалось получить серверы подписки %s для уменьшения счетчика: %s',
sub_id,
error,
)
connected_squads = list(subscription.connected_squads or []) connected_squads = list(subscription.connected_squads or [])
if connected_squads: if connected_squads:
@@ -657,7 +653,11 @@ async def decrement_subscription_server_counts(
squad_server_ids = await get_server_ids_by_uuids(db, connected_squads) squad_server_ids = await get_server_ids_by_uuids(db, connected_squads)
server_ids.update(squad_server_ids) server_ids.update(squad_server_ids)
except Exception as error: except Exception as error:
logger.error('⚠️ Не удалось сопоставить сквады подписки с серверами', sub_id=sub_id, error=error) logger.error(
'⚠️ Не удалось сопоставить сквады подписки %s с серверами: %s',
sub_id,
error,
)
if not server_ids: if not server_ids:
return return
@@ -670,16 +670,16 @@ async def decrement_subscription_server_counts(
await remove_user_from_servers(db, list(server_ids)) await remove_user_from_servers(db, list(server_ids))
except StaleDataError: except StaleDataError:
logger.warning( logger.warning(
'⚠️ Подписка уже удалена (StaleDataError), пропускаем декремент серверов', '⚠️ Подписка %s уже удалена (StaleDataError), пропускаем декремент серверов %s',
sub_id=sub_id, sub_id,
list=list(server_ids), list(server_ids),
) )
except Exception as error: except Exception as error:
logger.error( logger.error(
'⚠️ Ошибка уменьшения счетчика пользователей серверов для подписки', '⚠️ Ошибка уменьшения счетчика пользователей серверов %s для подписки %s: %s',
list=list(server_ids), list(server_ids),
sub_id=sub_id, sub_id,
error=error, error,
) )
@@ -694,7 +694,7 @@ async def update_subscription_autopay(
await db.refresh(subscription) await db.refresh(subscription)
status = 'включен' if enabled else 'выключен' status = 'включен' if enabled else 'выключен'
logger.info('💳 Автоплатеж для подписки пользователя', user_id=subscription.user_id, status=status) logger.info(f'💳 Автоплатеж для подписки пользователя {subscription.user_id} {status}')
return subscription return subscription
@@ -705,7 +705,7 @@ async def deactivate_subscription(db: AsyncSession, subscription: Subscription)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info('❌ Подписка пользователя деактивирована', user_id=subscription.user_id) logger.info(f'❌ Подписка пользователя {subscription.user_id} деактивирована')
return subscription return subscription
@@ -844,9 +844,9 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
renewals_count = conversion_stats.get('month_conversions', 0) renewals_count = conversion_stats.get('month_conversions', 0)
logger.info('📊 Статистика конверсии из таблицы conversions:') logger.info('📊 Статистика конверсии из таблицы conversions:')
logger.info('Общее количество конверсий', get=conversion_stats.get('total_conversions', 0)) logger.info(f' Общее количество конверсий: {conversion_stats.get("total_conversions", 0)}')
logger.info('Процент конверсии', trial_to_paid_conversion=trial_to_paid_conversion) logger.info(f' Процент конверсии: {trial_to_paid_conversion}%')
logger.info('Конверсий за месяц', renewals_count=renewals_count) logger.info(f' Конверсий за месяц: {renewals_count}')
except ImportError: except ImportError:
logger.warning('⚠️ Таблица subscription_conversions не найдена, используем старую логику') logger.warning('⚠️ Таблица subscription_conversions не найдена, используем старую логику')
@@ -943,7 +943,9 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
) )
except Exception as error: # pragma: no cover - defensive logging except Exception as error: # pragma: no cover - defensive logging
logger.error( logger.error(
'Не удалось обновить счётчики серверов при сбросе триала', subscription_id=subscription.id, error=error 'Не удалось обновить счётчики серверов при сбросе триала %s: %s',
subscription.id,
error,
) )
subscription_ids = [subscription.id for subscription in subscriptions] subscription_ids = [subscription.id for subscription in subscriptions]
@@ -952,7 +954,11 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
try: try:
await db.execute(delete(SubscriptionServer).where(SubscriptionServer.subscription_id.in_(subscription_ids))) await db.execute(delete(SubscriptionServer).where(SubscriptionServer.subscription_id.in_(subscription_ids)))
except Exception as error: # pragma: no cover - defensive logging except Exception as error: # pragma: no cover - defensive logging
logger.error('Ошибка удаления серверных связей триалов', subscription_ids=subscription_ids, error=error) logger.error(
'Ошибка удаления серверных связей триалов %s: %s',
subscription_ids,
error,
)
raise raise
await db.execute(delete(Subscription).where(Subscription.id.in_(subscription_ids))) await db.execute(delete(Subscription).where(Subscription.id.in_(subscription_ids)))
@@ -961,10 +967,10 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
await db.commit() await db.commit()
except Exception as error: # pragma: no cover - defensive logging except Exception as error: # pragma: no cover - defensive logging
await db.rollback() await db.rollback()
logger.error('Ошибка сохранения сброса триалов', error=error) logger.error('Ошибка сохранения сброса триалов: %s', error)
raise raise
logger.info('♻️ Сброшено триальных подписок', reset_count=reset_count) logger.info('♻️ Сброшено триальных подписок: %s', reset_count)
return reset_count return reset_count
@@ -1041,12 +1047,7 @@ async def add_subscription_servers(
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(f'🌐 К подписке {subscription.id} добавлено {len(server_squad_ids)} серверов с ценами: {paid_prices}')
'🌐 К подписке добавлено серверов с ценами',
subscription_id=subscription.id,
server_squad_ids_count=len(server_squad_ids),
paid_prices=paid_prices,
)
return subscription return subscription
@@ -1083,7 +1084,7 @@ async def get_servers_monthly_prices(
user_promo_group = user.get_primary_promo_group() user_promo_group = user.get_primary_promo_group()
user_promo_group_id = user_promo_group.id if user_promo_group else None user_promo_group_id = user_promo_group.id if user_promo_group else None
except Exception as e: except Exception as e:
logger.warning('Не удалось получить промогруппу пользователя', error=e) logger.warning(f'Не удалось получить промогруппу пользователя: {e}')
for server_id in server_squad_ids: for server_id in server_squad_ids:
# Загружаем сервер с промогруппами # Загружаем сервер с промогруппами
@@ -1109,11 +1110,9 @@ async def get_servers_monthly_prices(
else: else:
# Сервер недоступен для промогруппы пользователя # Сервер недоступен для промогруппы пользователя
logger.warning( logger.warning(
'⚠️ Сервер (id=) недоступен для промогруппы пользователя (promo_group_id=), allowed_promo_groups', f'⚠️ Сервер {server.display_name} (id={server_id}) недоступен для '
display_name=server.display_name, f'промогруппы пользователя (promo_group_id={user_promo_group_id}), '
server_id=server_id, f'allowed_promo_groups={[pg.id for pg in server.allowed_promo_groups] if server.allowed_promo_groups else []}'
user_promo_group_id=user_promo_group_id,
value=[pg.id for pg in server.allowed_promo_groups] if server.allowed_promo_groups else [],
) )
prices.append(server.price_kopeks) # Всё равно берём реальную цену prices.append(server.price_kopeks) # Всё равно берём реальную цену
@@ -1228,10 +1227,8 @@ async def calculate_subscription_total_cost(
], ],
} }
logger.debug( logger.debug(f'📊 Расчет стоимости подписки на {period_days} дней ({months_in_period} мес):')
'📊 Расчет стоимости подписки на дней ( мес)', period_days=period_days, months_in_period=months_in_period logger.debug(f' Базовый период: {base_price / 100}')
)
logger.debug('Базовый период: ₽', base_price=base_price / 100)
if total_traffic_price > 0: if total_traffic_price > 0:
message = f' Трафик: {traffic_price_per_month / 100}₽/мес × {months_in_period} = {total_traffic_price / 100}' message = f' Трафик: {traffic_price_per_month / 100}₽/мес × {months_in_period} = {total_traffic_price / 100}'
if total_traffic_discount > 0: if total_traffic_discount > 0:
@@ -1251,7 +1248,7 @@ async def calculate_subscription_total_cost(
if total_devices_discount > 0: if total_devices_discount > 0:
message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)' message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)'
logger.debug(message) logger.debug(message)
logger.debug('ИТОГО: ₽', total_cost=total_cost / 100) logger.debug(f' ИТОГО: {total_cost / 100}')
return total_cost, details return total_cost, details
@@ -1303,11 +1300,11 @@ async def remove_subscription_servers(db: AsyncSession, subscription_id: int, se
) )
await db.commit() await db.commit()
logger.info('🗑️ Удалены серверы из подписки', server_squad_ids=server_squad_ids, subscription_id=subscription_id) logger.info(f'🗑️ Удалены серверы {server_squad_ids} из подписки {subscription_id}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка удаления серверов из подписки', error=e) logger.error(f'Ошибка удаления серверов из подписки: {e}')
await db.rollback() await db.rollback()
return False return False
@@ -1398,13 +1395,8 @@ async def get_subscription_renewal_cost(
total_cost = base_price + total_servers_cost + total_traffic_cost + total_devices_cost total_cost = base_price + total_servers_cost + total_traffic_cost + total_devices_cost
logger.info( logger.info(f'💰 Расчет продления подписки {subscription_id} на {period_days} дней ({months_in_period} мес):')
'💰 Расчет продления подписки на дней ( мес)', logger.info(f' 📅 Период: {base_price / 100}')
subscription_id=subscription_id,
period_days=period_days,
months_in_period=months_in_period,
)
logger.info('📅 Период: ₽', base_price=base_price / 100)
if total_servers_cost > 0: if total_servers_cost > 0:
message = f' 🌍 Серверы: {servers_price_per_month / 100}₽/мес × {months_in_period} = {total_servers_cost / 100}' message = f' 🌍 Серверы: {servers_price_per_month / 100}₽/мес × {months_in_period} = {total_servers_cost / 100}'
if total_servers_discount > 0: if total_servers_discount > 0:
@@ -1422,12 +1414,12 @@ async def get_subscription_renewal_cost(
if total_devices_discount > 0: if total_devices_discount > 0:
message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)' message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)'
logger.info(message) logger.info(message)
logger.info('💎 ИТОГО: ₽', total_cost=total_cost / 100) logger.info(f' 💎 ИТОГО: {total_cost / 100}')
return total_cost return total_cost
except Exception as e: except Exception as e:
logger.error('Ошибка расчета стоимости продления', error=e) logger.error(f'Ошибка расчета стоимости продления: {e}')
from app.config import PERIOD_PRICES from app.config import PERIOD_PRICES
return PERIOD_PRICES.get(period_days, 0) return PERIOD_PRICES.get(period_days, 0)
@@ -1516,7 +1508,7 @@ async def calculate_addon_cost_for_remaining_period(
) )
logger.info(message) logger.info(message)
logger.info('💰 Итого доплата за мес: ₽', months_to_pay=months_to_pay, total_cost=total_cost / 100) logger.info(f'💰 Итого доплата за {months_to_pay} мес: {total_cost / 100}')
return total_cost return total_cost
@@ -1527,7 +1519,7 @@ async def expire_subscription(db: AsyncSession, subscription: Subscription) -> S
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info('⏰ Подписка пользователя помечена как истёкшая', user_id=subscription.user_id) logger.info(f'⏰ Подписка пользователя {subscription.user_id} помечена как истёкшая')
return subscription return subscription
@@ -1535,30 +1527,28 @@ async def check_and_update_subscription_status(db: AsyncSession, subscription: S
current_time = datetime.utcnow() current_time = datetime.utcnow()
logger.info( logger.info(
'🔍 Проверка статуса подписки , текущий статус дата окончания текущее время', '🔍 Проверка статуса подписки %s, текущий статус: %s, дата окончания: %s, текущее время: %s',
subscription_id=subscription.id, subscription.id,
subscription_status=subscription.status, subscription.status,
format_local_datetime=format_local_datetime(subscription.end_date), format_local_datetime(subscription.end_date),
format_local_datetime_2=format_local_datetime(current_time), format_local_datetime(current_time),
) )
# Для суточных тарифов с паузой не меняем статус на expired # Для суточных тарифов с паузой не меняем статус на expired
# (время "заморожено" пока пользователь на паузе) # (время "заморожено" пока пользователь на паузе)
is_daily_paused = getattr(subscription, 'is_daily_paused', False) is_daily_paused = getattr(subscription, 'is_daily_paused', False)
if is_daily_paused: if is_daily_paused:
logger.info('⏸️ Суточная подписка на паузе, пропускаем проверку истечения', subscription_id=subscription.id) logger.info(f'⏸️ Суточная подписка {subscription.id} на паузе, пропускаем проверку истечения')
return subscription return subscription
if subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date <= current_time: if subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date <= current_time:
# Детальное логирование для отладки проблемы с деактивацией # Детальное логирование для отладки проблемы с деактивацией
time_diff = current_time - subscription.end_date time_diff = current_time - subscription.end_date
logger.warning( logger.warning(
'⏰ DEACTIVATION: подписка (user_id=) деактивируется в check_and_update_subscription_status. end_date=, current_time=, просрочена на', f'⏰ DEACTIVATION: подписка {subscription.id} (user_id={subscription.user_id}) '
subscription_id=subscription.id, f'деактивируется в check_and_update_subscription_status. '
user_id=subscription.user_id, f'end_date={subscription.end_date}, current_time={current_time}, '
end_date=subscription.end_date, f'просрочена на {time_diff}'
current_time=current_time,
time_diff=time_diff,
) )
subscription.status = SubscriptionStatus.EXPIRED.value subscription.status = SubscriptionStatus.EXPIRED.value
@@ -1567,9 +1557,9 @@ async def check_and_update_subscription_status(db: AsyncSession, subscription: S
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info("⏰ Статус подписки пользователя изменен на 'expired'", user_id=subscription.user_id) logger.info(f"⏰ Статус подписки пользователя {subscription.user_id} изменен на 'expired'")
elif subscription.status == SubscriptionStatus.PENDING.value: elif subscription.status == SubscriptionStatus.PENDING.value:
logger.info('ℹ️ Проверка PENDING подписки статус остается без изменений', subscription_id=subscription.id) logger.info(f'ℹ️ Проверка PENDING подписки {subscription.id}, статус остается без изменений')
return subscription return subscription
@@ -1624,7 +1614,7 @@ async def create_subscription_no_commit(
await db.flush() await db.flush()
# Не коммитим сразу, оставляем для пакетной обработки # Не коммитим сразу, оставляем для пакетной обработки
logger.info('✅ Подготовлена подписка для пользователя (ожидает коммита)', user_id=user_id) logger.info(f'✅ Подготовлена подписка для пользователя {user_id} (ожидает коммита)')
return subscription return subscription
@@ -1672,7 +1662,7 @@ async def create_subscription(
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info('✅ Создана подписка для пользователя', user_id=user_id) logger.info(f'✅ Создана подписка для пользователя {user_id}')
return subscription return subscription
@@ -1704,9 +1694,9 @@ async def create_pending_subscription(
and existing_subscription.end_date > current_time and existing_subscription.end_date > current_time
): ):
logger.warning( logger.warning(
'⚠️ Попытка создать pending подписку для активного пользователя . Возвращаем существующую запись.', '⚠️ Попытка создать pending %sподписку для активного пользователя %s. Возвращаем существующую запись.',
trial_label=trial_label, trial_label,
user_id=user_id, user_id,
) )
return existing_subscription return existing_subscription
@@ -1724,11 +1714,11 @@ async def create_pending_subscription(
await db.refresh(existing_subscription) await db.refresh(existing_subscription)
logger.info( logger.info(
'♻️ Обновлена ожидающая подписка пользователя , ID метод оплаты', '♻️ Обновлена ожидающая %sподписка пользователя %s, ID: %s, метод оплаты: %s',
trial_label=trial_label, trial_label,
user_id=user_id, user_id,
existing_subscription_id=existing_subscription.id, existing_subscription.id,
payment_method=payment_method, payment_method,
) )
return existing_subscription return existing_subscription
@@ -1750,11 +1740,11 @@ async def create_pending_subscription(
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(
'💳 Создана ожидающая подписка для пользователя , ID метод оплаты', '💳 Создана ожидающая %sподписка для пользователя %s, ID: %s, метод оплаты: %s',
trial_label=trial_label, trial_label,
user_id=user_id, user_id,
subscription_id=subscription.id, subscription.id,
payment_method=payment_method, payment_method,
) )
return subscription return subscription
@@ -1787,7 +1777,7 @@ async def create_pending_trial_subscription(
async def activate_pending_subscription(db: AsyncSession, user_id: int, period_days: int = None) -> Subscription | None: async def activate_pending_subscription(db: AsyncSession, user_id: int, period_days: int = None) -> Subscription | None:
"""Активирует pending подписку пользователя, меняя её статус на ACTIVE.""" """Активирует pending подписку пользователя, меняя её статус на ACTIVE."""
logger.info('Активация pending подписки: пользователь период дней', user_id=user_id, period_days=period_days) logger.info(f'Активация pending подписки: пользователь {user_id}, период {period_days} дней')
# Находим pending подписку пользователя # Находим pending подписку пользователя
result = await db.execute( result = await db.execute(
@@ -1798,14 +1788,11 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
pending_subscription = result.scalar_one_or_none() pending_subscription = result.scalar_one_or_none()
if not pending_subscription: if not pending_subscription:
logger.warning('Не найдена pending подписка для пользователя', user_id=user_id) logger.warning(f'Не найдена pending подписка для пользователя {user_id}')
return None return None
logger.info( logger.info(
'Найдена pending подписка для пользователя статус', f'Найдена pending подписка {pending_subscription.id} для пользователя {user_id}, статус: {pending_subscription.status}'
pending_subscription_id=pending_subscription.id,
user_id=user_id,
status=pending_subscription.status,
) )
# Обновляем статус подписки на ACTIVE # Обновляем статус подписки на ACTIVE
@@ -1825,9 +1812,7 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
await db.commit() await db.commit()
await db.refresh(pending_subscription) await db.refresh(pending_subscription)
logger.info( logger.info(f'Подписка пользователя {user_id} активирована, ID: {pending_subscription.id}')
'Подписка пользователя активирована, ID', user_id=user_id, pending_subscription_id=pending_subscription.id
)
return pending_subscription return pending_subscription
@@ -1838,11 +1823,7 @@ async def activate_pending_trial_subscription(
user_id: int, user_id: int,
) -> Subscription | None: ) -> Subscription | None:
"""Активирует pending триальную подписку по её ID после оплаты.""" """Активирует pending триальную подписку по её ID после оплаты."""
logger.info( logger.info(f'Активация pending триальной подписки: subscription_id={subscription_id}, user_id={user_id}')
'Активация pending триальной подписки: subscription_id=, user_id',
subscription_id=subscription_id,
user_id=user_id,
)
# Находим pending подписку по ID # Находим pending подписку по ID
result = await db.execute( result = await db.execute(
@@ -1858,16 +1839,10 @@ async def activate_pending_trial_subscription(
pending_subscription = result.scalar_one_or_none() pending_subscription = result.scalar_one_or_none()
if not pending_subscription: if not pending_subscription:
logger.warning( logger.warning(f'Не найдена pending триальная подписка {subscription_id} для пользователя {user_id}')
'Не найдена pending триальная подписка для пользователя', subscription_id=subscription_id, user_id=user_id
)
return None return None
logger.info( logger.info(f'Найдена pending триальная подписка {pending_subscription.id}, статус: {pending_subscription.status}')
'Найдена pending триальная подписка статус',
pending_subscription_id=pending_subscription.id,
status=pending_subscription.status,
)
# Обновляем статус подписки на ACTIVE # Обновляем статус подписки на ACTIVE
current_time = datetime.utcnow() current_time = datetime.utcnow()
@@ -1890,11 +1865,7 @@ async def activate_pending_trial_subscription(
await db.commit() await db.commit()
await db.refresh(pending_subscription) await db.refresh(pending_subscription)
logger.info( logger.info(f'Триальная подписка {pending_subscription.id} активирована для пользователя {user_id}')
'Триальная подписка активирована для пользователя',
pending_subscription_id=pending_subscription.id,
user_id=user_id,
)
return pending_subscription return pending_subscription
@@ -1940,7 +1911,7 @@ async def get_daily_subscriptions_for_charge(db: AsyncSession) -> list[Subscript
result = await db.execute(query) result = await db.execute(query)
subscriptions = result.scalars().all() subscriptions = result.scalars().all()
logger.info('🔍 Найдено суточных подписок для списания', subscriptions_count=len(subscriptions)) logger.info(f'🔍 Найдено {len(subscriptions)} суточных подписок для списания')
return list(subscriptions) return list(subscriptions)
@@ -1977,7 +1948,7 @@ async def get_disabled_daily_subscriptions_for_resume(
result = await db.execute(query) result = await db.execute(query)
subscriptions = result.scalars().all() subscriptions = result.scalars().all()
logger.info('🔍 Найдено DISABLED суточных подписок для возобновления', subscriptions_count=len(subscriptions)) logger.info(f'🔍 Найдено {len(subscriptions)} DISABLED суточных подписок для возобновления')
return list(subscriptions) return list(subscriptions)
@@ -1988,18 +1959,14 @@ async def pause_daily_subscription(
) -> Subscription: ) -> Subscription:
"""Приостанавливает суточную подписку (списание не будет происходить).""" """Приостанавливает суточную подписку (списание не будет происходить)."""
if not subscription.is_daily_tariff: if not subscription.is_daily_tariff:
logger.warning('Попытка приостановить не-суточную подписку', subscription_id=subscription.id) logger.warning(f'Попытка приостановить не-суточную подписку {subscription.id}')
return subscription return subscription
subscription.is_daily_paused = True subscription.is_daily_paused = True
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(f'⏸️ Суточная подписка {subscription.id} приостановлена пользователем {subscription.user_id}')
'⏸️ Суточная подписка приостановлена пользователем',
subscription_id=subscription.id,
user_id=subscription.user_id,
)
return subscription return subscription
@@ -2010,7 +1977,7 @@ async def resume_daily_subscription(
) -> Subscription: ) -> Subscription:
"""Возобновляет суточную подписку (списание продолжится).""" """Возобновляет суточную подписку (списание продолжится)."""
if not subscription.is_daily_tariff: if not subscription.is_daily_tariff:
logger.warning('Попытка возобновить не-суточную подписку', subscription_id=subscription.id) logger.warning(f'Попытка возобновить не-суточную подписку {subscription.id}')
return subscription return subscription
subscription.is_daily_paused = False subscription.is_daily_paused = False
@@ -2022,18 +1989,12 @@ async def resume_daily_subscription(
# Обновляем время последнего списания для корректного расчёта следующего # Обновляем время последнего списания для корректного расчёта следующего
subscription.last_daily_charge_at = datetime.utcnow() subscription.last_daily_charge_at = datetime.utcnow()
subscription.end_date = datetime.utcnow() + timedelta(days=1) subscription.end_date = datetime.utcnow() + timedelta(days=1)
logger.info( logger.info(f'✅ Суточная подписка {subscription.id} восстановлена из {previous_status} в ACTIVE')
'✅ Суточная подписка восстановлена из в ACTIVE',
subscription_id=subscription.id,
previous_status=previous_status,
)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(f'▶️ Суточная подписка {subscription.id} возобновлена пользователем {subscription.user_id}')
'▶️ Суточная подписка возобновлена пользователем', subscription_id=subscription.id, user_id=subscription.user_id
)
return subscription return subscription
@@ -2051,7 +2012,7 @@ async def update_daily_charge_time(
new_end_date = now + timedelta(days=1) new_end_date = now + timedelta(days=1)
if subscription.end_date is None or subscription.end_date < new_end_date: if subscription.end_date is None or subscription.end_date < new_end_date:
subscription.end_date = new_end_date subscription.end_date = new_end_date
logger.info('📅 Продлена подписка до', subscription_id=subscription.id, new_end_date=new_end_date) logger.info(f'📅 Продлена подписка {subscription.id} до {new_end_date}')
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -2072,9 +2033,7 @@ async def suspend_daily_subscription_insufficient_balance(
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(
'⚠️ Суточная подписка приостановлена: недостаточно средств (user_id=)', f'⚠️ Суточная подписка {subscription.id} приостановлена: недостаточно средств (user_id={subscription.user_id})'
subscription_id=subscription.id,
user_id=subscription.user_id,
) )
return subscription return subscription
+6 -10
View File
@@ -1,13 +1,13 @@
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import structlog
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import SubscriptionConversion, User from app.database.models import SubscriptionConversion, User
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_subscription_conversion( async def create_subscription_conversion(
@@ -32,11 +32,7 @@ async def create_subscription_conversion(
await db.refresh(conversion) await db.refresh(conversion)
logger.info( logger.info(
'✅ Создана запись о конверсии для пользователя дн. → дн. за ', f'✅ Создана запись о конверсии для пользователя {user_id}: {trial_duration_days} дн. → {first_paid_period_days} дн. за {first_payment_amount_kopeks / 100}'
user_id=user_id,
trial_duration_days=trial_duration_days,
first_paid_period_days=first_paid_period_days,
first_payment_amount_kopeks=first_payment_amount_kopeks / 100,
) )
return conversion return conversion
@@ -89,9 +85,9 @@ async def get_conversion_statistics(db: AsyncSession) -> dict:
month_conversions = month_conversions_result.scalar() or 0 month_conversions = month_conversions_result.scalar() or 0
logger.info('📊 Статистика конверсий:') logger.info('📊 Статистика конверсий:')
logger.info('Всего пользователей с подписками', total_users_with_subscriptions=total_users_with_subscriptions) logger.info(f' Всего пользователей с подписками: {total_users_with_subscriptions}')
logger.info('Оплативших подписку', users_with_paid=users_with_paid) logger.info(f' Оплативших подписку: {users_with_paid}')
logger.info('Рассчитанная конверсия', conversion_rate=conversion_rate) logger.info(f' Рассчитанная конверсия: {conversion_rate}%')
return { return {
'total_conversions': total_conversions, 'total_conversions': total_conversions,
+26 -22
View File
@@ -1,4 +1,5 @@
import structlog import logging
from sqlalchemy import func, select, update from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -6,7 +7,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoGroup, Subscription, Tariff from app.database.models import PromoGroup, Subscription, Tariff
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
def _normalize_period_prices(period_prices: dict[int, int] | None) -> dict[str, int]: def _normalize_period_prices(period_prices: dict[int, int] | None) -> dict[str, int]:
@@ -240,13 +241,13 @@ async def create_tariff(
await db.refresh(tariff) await db.refresh(tariff)
logger.info( logger.info(
"Создан тариф '' (id tier traffic=GB, devices prices=)", "Создан тариф '%s' (id=%s, tier=%s, traffic=%sGB, devices=%s, prices=%s)",
tariff_name=tariff.name, tariff.name,
tariff_id=tariff.id, tariff.id,
tier_level=tariff.tier_level, tariff.tier_level,
traffic_limit_gb=tariff.traffic_limit_gb, tariff.traffic_limit_gb,
device_limit=tariff.device_limit, tariff.device_limit,
normalized_prices=normalized_prices, normalized_prices,
) )
return tariff return tariff
@@ -364,7 +365,11 @@ async def update_tariff(
await db.commit() await db.commit()
await db.refresh(tariff) await db.refresh(tariff)
logger.info("Обновлен тариф '' (id=)", tariff_name=tariff.name, tariff_id=tariff.id) logger.info(
"Обновлен тариф '%s' (id=%s)",
tariff.name,
tariff.id,
)
return tariff return tariff
@@ -388,10 +393,10 @@ async def delete_tariff(db: AsyncSession, tariff: Tariff) -> bool:
await db.commit() await db.commit()
logger.info( logger.info(
"Удален тариф '' (id=), затронуто подписок", "Удален тариф '%s' (id=%s), затронуто подписок: %s",
tariff_name=tariff_name, tariff_name,
tariff_id=tariff_id, tariff_id,
affected_subscriptions=affected_subscriptions, affected_subscriptions,
) )
return True return True
@@ -481,7 +486,7 @@ async def reorder_tariffs(
for order, tariff_id in enumerate(tariff_order): for order, tariff_id in enumerate(tariff_order):
await db.execute(update(Tariff).where(Tariff.id == tariff_id).values(display_order=order)) await db.execute(update(Tariff).where(Tariff.id == tariff_id).values(display_order=order))
logger.info('Изменен порядок тарифов', tariff_order=tariff_order) logger.info('Изменен порядок тарифов: %s', tariff_order)
async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None: async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
@@ -517,8 +522,7 @@ async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
# Тариф уже существует — НЕ перезаписываем настройки из конфига. # Тариф уже существует — НЕ перезаписываем настройки из конфига.
# Админ управляет тарифом через кабинет, синхронизация не нужна. # Админ управляет тарифом через кабинет, синхронизация не нужна.
logger.info( logger.info(
"Дефолтный тариф 'Стандартный' (id=) уже существует, пропускаем sync из конфига", "Дефолтный тариф 'Стандартный' (id=%s) уже существует, пропускаем sync из конфига", existing_tariff.id
existing_tariff_id=existing_tariff.id,
) )
return existing_tariff return existing_tariff
@@ -540,7 +544,7 @@ async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
db.add(new_tariff) db.add(new_tariff)
await db.commit() await db.commit()
await db.refresh(new_tariff) await db.refresh(new_tariff)
logger.info("Создан дефолтный тариф 'Стандартный' из конфига", period_prices=period_prices) logger.info("Создан дефолтный тариф 'Стандартный' из конфига: %s", period_prices)
return new_tariff return new_tariff
return None return None
@@ -570,7 +574,7 @@ async def load_period_prices_from_db(db: AsyncSession) -> None:
return return
if not tariff.period_prices: if not tariff.period_prices:
logger.warning("Тариф '' (id=) найден, но period_prices пуст", tariff_name=tariff.name, tariff_id=tariff.id) logger.warning("Тариф '%s' (id=%s) найден, но period_prices пуст", tariff.name, tariff.id)
return return
# Преобразуем строковые ключи в int # Преобразуем строковые ключи в int
@@ -584,10 +588,10 @@ async def load_period_prices_from_db(db: AsyncSession) -> None:
{f'{d}д': f'{p // 100}' for d, p in period_prices.items()}, {f'{d}д': f'{p // 100}' for d, p in period_prices.items()},
) )
else: else:
logger.warning("Тариф '' не имеет активных периодов (все цены = 0)", tariff_name=tariff.name) logger.warning("Тариф '%s' не имеет активных периодов (все цены = 0)", tariff.name)
except Exception as e: except Exception as e:
logger.error('Ошибка загрузки периодов из БД', e=e) logger.error('Ошибка загрузки периодов из БД: %s', e)
async def ensure_tariffs_synced(db: AsyncSession) -> None: async def ensure_tariffs_synced(db: AsyncSession) -> None:
@@ -600,4 +604,4 @@ async def ensure_tariffs_synced(db: AsyncSession) -> None:
# Загружаем периоды из БД в PERIOD_PRICES # Загружаем периоды из БД в PERIOD_PRICES
await load_period_prices_from_db(db) await load_period_prices_from_db(db)
except Exception as e: except Exception as e:
logger.error('Ошибка синхронизации тарифов', e=e) logger.error('Ошибка синхронизации тарифов: %s', e)
+5 -5
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import and_, desc, func, or_, select, update from sqlalchemy import and_, desc, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import SupportAuditLog, Ticket, TicketMessage, TicketStatus from app.database.models import SupportAuditLog, Ticket, TicketMessage, TicketStatus
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class TicketCRUD: class TicketCRUD:
@@ -64,7 +64,7 @@ class TicketCRUD:
db=db, db=db,
) )
except Exception as error: except Exception as error:
logger.warning('Failed to emit ticket.created event', error=error) logger.warning('Failed to emit ticket.created event: %s', error)
return ticket return ticket
@@ -248,7 +248,7 @@ class TicketCRUD:
db=db, db=db,
) )
except Exception as error: except Exception as error:
logger.warning('Failed to emit ticket.status_changed event', error=error) logger.warning('Failed to emit ticket.status_changed event: %s', error)
return True return True
@@ -440,7 +440,7 @@ class TicketMessageCRUD:
db=db, db=db,
) )
except Exception as error: except Exception as error:
logger.warning('Failed to emit ticket.message_added event', error=error) logger.warning('Failed to emit ticket.message_added event: %s', error)
return message return message
+2 -2
View File
@@ -1,8 +1,8 @@
"""CRUD operations for TicketNotification.""" """CRUD operations for TicketNotification."""
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import desc, func, select, update from sqlalchemy import desc, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -10,7 +10,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import Ticket, TicketNotification from app.database.models import Ticket, TicketNotification
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class TicketNotificationCRUD: class TicketNotificationCRUD:
+20 -15
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import structlog
from sqlalchemy import and_, func, or_, select from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PaymentMethod, Transaction, TransactionType, User from app.database.models import PaymentMethod, Transaction, TransactionType, User
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
# Реальные платёжные методы для подсчёта дохода # Реальные платёжные методы для подсчёта дохода
# Исключены: MANUAL (админские), BALANCE (оплата с баланса), NULL (колесо, промокоды, бонусы) # Исключены: MANUAL (админские), BALANCE (оплата с баланса), NULL (колесо, промокоды, бонусы)
@@ -55,12 +55,7 @@ async def create_transaction(
await db.commit() await db.commit()
await db.refresh(transaction) await db.refresh(transaction)
logger.info( logger.info(f'💳 Создана транзакция: {type.value} на {amount_kopeks / 100}₽ для пользователя {user_id}')
'💳 Создана транзакция: на ₽ для пользователя',
type_value=type.value,
amount_kopeks=amount_kopeks / 100,
user_id=user_id,
)
# Отправляем событие о транзакции # Отправляем событие о транзакции
try: try:
@@ -82,7 +77,7 @@ async def create_transaction(
db=db, db=db,
) )
except Exception as error: except Exception as error:
logger.warning('Failed to emit transaction event', error=error) logger.warning('Failed to emit transaction event: %s', error)
try: try:
from app.services.promo_group_assignment import ( from app.services.promo_group_assignment import (
@@ -91,7 +86,11 @@ async def create_transaction(
await maybe_assign_promo_group_by_total_spent(db, user_id) await maybe_assign_promo_group_by_total_spent(db, user_id)
except Exception as exc: except Exception as exc:
logger.debug('Не удалось проверить автовыдачу промогруппы для пользователя', user_id=user_id, exc=exc) logger.debug(
'Не удалось проверить автовыдачу промогруппы для пользователя %s: %s',
user_id,
exc,
)
if type == TransactionType.SUBSCRIPTION_PAYMENT: if type == TransactionType.SUBSCRIPTION_PAYMENT:
try: try:
from app.services.referral_contest_service import referral_contest_service from app.services.referral_contest_service import referral_contest_service
@@ -102,7 +101,11 @@ async def create_transaction(
amount_kopeks, amount_kopeks,
) )
except Exception as exc: except Exception as exc:
logger.debug('Не удалось записать событие конкурса для пользователя', user_id=user_id, exc=exc) logger.debug(
'Не удалось записать событие конкурса для пользователя %s: %s',
user_id,
exc,
)
return transaction return transaction
@@ -168,7 +171,7 @@ async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Tr
await db.commit() await db.commit()
await db.refresh(transaction) await db.refresh(transaction)
logger.info('✅ Транзакция завершена', transaction_id=transaction.id) logger.info(f'✅ Транзакция {transaction.id} завершена')
try: try:
from app.services.promo_group_assignment import ( from app.services.promo_group_assignment import (
@@ -178,7 +181,9 @@ async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Tr
await maybe_assign_promo_group_by_total_spent(db, transaction.user_id) await maybe_assign_promo_group_by_total_spent(db, transaction.user_id)
except Exception as exc: except Exception as exc:
logger.debug( logger.debug(
'Не удалось проверить автовыдачу промогруппы для пользователя', user_id=transaction.user_id, exc=exc 'Не удалось проверить автовыдачу промогруппы для пользователя %s: %s',
transaction.user_id,
exc,
) )
return transaction return transaction
@@ -387,7 +392,7 @@ async def check_tribute_payment_duplicate(
transaction = result.scalar_one_or_none() transaction = result.scalar_one_or_none()
if transaction: if transaction:
logger.info('🔍 Найден дубликат платежа в течение 24ч', transaction_id=transaction.id) logger.info(f'🔍 Найден дубликат платежа в течение 24ч: {transaction.id}')
return transaction return transaction
@@ -403,7 +408,7 @@ async def create_unique_tribute_transaction(
timestamp = int(datetime.utcnow().timestamp()) timestamp = int(datetime.utcnow().timestamp())
external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}' external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}'
logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id) logger.info(f'Создан уникальный external_id для избежания дубликатов: {external_id}')
return await create_transaction( return await create_transaction(
db=db, db=db,
+52 -55
View File
@@ -1,8 +1,8 @@
import logging
import secrets import secrets
import string import string
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog
from sqlalchemy import and_, case, func, nullslast, or_, select, text from sqlalchemy import and_, case, func, nullslast, or_, select, text
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -25,7 +25,7 @@ from app.database.models import (
from app.utils.validators import sanitize_telegram_name from app.utils.validators import sanitize_telegram_name
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
def _normalize_language_code(language: str | None, fallback: str = 'ru') -> str: def _normalize_language_code(language: str | None, fallback: str = 'ru') -> str:
@@ -269,11 +269,7 @@ async def create_user_no_commit(
user.promo_group = default_group user.promo_group = default_group
# Не коммитим сразу, оставляем для пакетной обработки # Не коммитим сразу, оставляем для пакетной обработки
logger.info( logger.info(f'✅ Подготовлен пользователь {telegram_id} с реферальным кодом {referral_code} (ожидает коммита)')
'✅ Подготовлен пользователь с реферальным кодом (ожидает коммита)',
telegram_id=telegram_id,
referral_code=referral_code,
)
return user return user
@@ -320,9 +316,7 @@ async def create_user(
await db.refresh(user) await db.refresh(user)
user.promo_group = default_group user.promo_group = default_group
logger.info( logger.info(f'✅ Создан пользователь {telegram_id} с реферальным кодом {referral_code}')
'✅ Создан пользователь с реферальным кодом', telegram_id=telegram_id, referral_code=referral_code
)
# Отправляем событие о создании пользователя # Отправляем событие о создании пользователя
try: try:
@@ -342,7 +336,7 @@ async def create_user(
db=db, db=db,
) )
except Exception as error: except Exception as error:
logger.warning('Failed to emit user.created event', error=error) logger.warning('Failed to emit user.created event: %s', error)
return user return user
@@ -355,10 +349,11 @@ async def create_user(
and attempt < attempts and attempt < attempts
): ):
logger.warning( logger.warning(
'⚠️ Обнаружено несоответствие последовательности users_id_seq при создании пользователя . Выполняем повторную синхронизацию (попытка /)', '⚠️ Обнаружено несоответствие последовательности users_id_seq при создании пользователя %s. '
telegram_id=telegram_id, 'Выполняем повторную синхронизацию (попытка %s/%s)',
attempt=attempt, telegram_id,
attempts=attempts, attempt,
attempts,
) )
await _sync_users_sequence(db) await _sync_users_sequence(db)
continue continue
@@ -418,11 +413,7 @@ async def add_user_balance(
user_id_display = user.telegram_id or user.email or f'#{user.id}' user_id_display = user.telegram_id or user.email or f'#{user.id}'
logger.info( logger.info(
'💰 Баланс пользователя изменен: → (изменение: +)', f'💰 Баланс пользователя {user_id_display} изменен: {old_balance}{user.balance_kopeks} (изменение: +{amount_kopeks})'
user_id_display=user_id_display,
old_balance=old_balance,
balance_kopeks=user.balance_kopeks,
amount_kopeks=amount_kopeks,
) )
# Автоматическое возобновление приостановленной суточной подписки # Автоматическое возобновление приостановленной суточной подписки
@@ -445,9 +436,8 @@ async def add_user_balance(
if daily_price > 0 and user.balance_kopeks >= daily_price: if daily_price > 0 and user.balance_kopeks >= daily_price:
await resume_daily_subscription(db, subscription) await resume_daily_subscription(db, subscription)
logger.info( logger.info(
'✅ Автоматически возобновлена суточная подписка после пополнения баланса (user_id=)', f'✅ Автоматически возобновлена суточная подписка {subscription.id} '
subscription_id=subscription.id, f'после пополнения баланса (user_id={user.id})'
user_id=user.id,
) )
# Синхронизируем с RemnaWave # Синхронизируем с RemnaWave
try: try:
@@ -456,14 +446,14 @@ async def add_user_balance(
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription) await subscription_service.update_remnawave_user(db, subscription)
except Exception as sync_err: except Exception as sync_err:
logger.warning('Не удалось синхронизировать с RemnaWave', sync_err=sync_err) logger.warning(f'Не удалось синхронизировать с RemnaWave: {sync_err}')
except Exception as resume_err: except Exception as resume_err:
logger.warning('Ошибка при попытке возобновить суточную подписку', resume_err=resume_err) logger.warning(f'Ошибка при попытке возобновить суточную подписку: {resume_err}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка изменения баланса пользователя', user_id=user.id, error=e) logger.error(f'Ошибка изменения баланса пользователя {user.id}: {e}')
await db.rollback() await db.rollback()
return False return False
@@ -479,7 +469,7 @@ async def add_user_balance_by_id(
try: try:
user = await get_user_by_telegram_id(db, telegram_id) user = await get_user_by_telegram_id(db, telegram_id)
if not user: if not user:
logger.error('Пользователь с telegram_id не найден', telegram_id=telegram_id) logger.error(f'Пользователь с telegram_id {telegram_id} не найден')
return False return False
return await add_user_balance( return await add_user_balance(
@@ -492,7 +482,7 @@ async def add_user_balance_by_id(
) )
except Exception as e: except Exception as e:
logger.error('Ошибка пополнения баланса пользователя', telegram_id=telegram_id, error=e) logger.error(f'Ошибка пополнения баланса пользователя {telegram_id}: {e}')
return False return False
@@ -508,10 +498,10 @@ async def subtract_user_balance(
) -> bool: ) -> bool:
user_id_display = user.telegram_id or user.email or f'#{user.id}' user_id_display = user.telegram_id or user.email or f'#{user.id}'
logger.info('💸 ОТЛАДКА subtract_user_balance:') logger.info('💸 ОТЛАДКА subtract_user_balance:')
logger.info('👤 User ID: (ID: )', user_id=user.id, user_id_display=user_id_display) logger.info(f' 👤 User ID: {user.id} (ID: {user_id_display})')
logger.info('💰 Баланс до списания: копеек', balance_kopeks=user.balance_kopeks) logger.info(f' 💰 Баланс до списания: {user.balance_kopeks} копеек')
logger.info('💸 Сумма к списанию: копеек', amount_kopeks=amount_kopeks) logger.info(f' 💸 Сумма к списанию: {amount_kopeks} копеек')
logger.info('📝 Описание', description=description) logger.info(f' 📝 Описание: {description}')
# Lock the user row to prevent concurrent balance race conditions # Lock the user row to prevent concurrent balance race conditions
locked_result = await db.execute(select(User).where(User.id == user.id).with_for_update()) locked_result = await db.execute(select(User).where(User.id == user.id).with_for_update())
@@ -541,7 +531,9 @@ async def subtract_user_balance(
offer = await get_latest_claimed_offer_for_user(db, user.id, source) offer = await get_latest_claimed_offer_for_user(db, user.id, source)
except Exception as lookup_error: # pragma: no cover - defensive logging except Exception as lookup_error: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to fetch latest claimed promo offer for user', user_id=user.id, lookup_error=lookup_error 'Failed to fetch latest claimed promo offer for user %s: %s',
user.id,
lookup_error,
) )
offer = None offer = None
@@ -600,21 +592,23 @@ async def subtract_user_balance(
) )
except Exception as log_error: # pragma: no cover - defensive logging except Exception as log_error: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to record promo offer consumption log for user', user_id=user.id, log_error=log_error 'Failed to record promo offer consumption log for user %s: %s',
user.id,
log_error,
) )
try: try:
await db.rollback() await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to rollback session after promo offer consumption log failure', 'Failed to rollback session after promo offer consumption log failure: %s',
rollback_error=rollback_error, rollback_error,
) )
logger.info('✅ Средства списаны: ', old_balance=old_balance, balance_kopeks=user.balance_kopeks) logger.info(f' ✅ Средства списаны: {old_balance}{user.balance_kopeks}')
return True return True
except Exception as e: except Exception as e:
logger.error('❌ ОШИБКА СПИСАНИЯ', error=e) logger.error(f' ❌ ОШИБКА СПИСАНИЯ: {e}')
await db.rollback() await db.rollback()
return False return False
@@ -649,9 +643,9 @@ async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
offer = await get_latest_claimed_offer_for_user(db, user.id, source) offer = await get_latest_claimed_offer_for_user(db, user.id, source)
except Exception as lookup_error: # pragma: no cover - defensive logging except Exception as lookup_error: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to fetch latest claimed promo offer for user during expiration cleanup', 'Failed to fetch latest claimed promo offer for user %s during expiration cleanup: %s',
user_id=user.id, user.id,
lookup_error=lookup_error, lookup_error,
) )
offer = None offer = None
@@ -694,12 +688,17 @@ async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
details={'reason': 'offer_expired'}, details={'reason': 'offer_expired'},
) )
except Exception as log_error: # pragma: no cover - defensive logging except Exception as log_error: # pragma: no cover - defensive logging
logger.warning('Failed to log promo offer expiration for user', user_id=user_id, log_error=log_error) logger.warning(
'Failed to log promo offer expiration for user %s: %s',
user_id,
log_error,
)
try: try:
await db.rollback() await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning( logger.warning(
'Failed to rollback session after promo offer expiration log failure', rollback_error=rollback_error 'Failed to rollback session after promo offer expiration log failure: %s',
rollback_error,
) )
return len(users) return len(users)
@@ -945,7 +944,7 @@ async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[Us
), ),
) )
else: else:
logger.warning('Неизвестный сегмент для промо', segment=segment) logger.warning('Неизвестный сегмент для промо: %s', segment)
return [] return []
result = await db.execute(query.order_by(User.id)) result = await db.execute(query.order_by(User.id))
@@ -990,7 +989,7 @@ async def delete_user(db: AsyncSession, user: User) -> bool:
await db.commit() await db.commit()
user_id_display = user.telegram_id or user.email or f'#{user.id}' user_id_display = user.telegram_id or user.email or f'#{user.id}'
logger.info('🗑️ Пользователь помечен как удаленный', user_id_display=user_id_display) logger.info(f'🗑️ Пользователь {user_id_display} помечен как удаленный')
return True return True
@@ -1105,7 +1104,7 @@ async def create_user_by_email(
await db.refresh(user) await db.refresh(user)
user.promo_group = default_group user.promo_group = default_group
logger.info('✅ Создан email-пользователь с id', email=email, user_id=user.id) logger.info(f'✅ Создан email-пользователь {email} с id={user.id}')
# Emit event # Emit event
try: try:
@@ -1124,7 +1123,7 @@ async def create_user_by_email(
db=db, db=db,
) )
except Exception as error: except Exception as error:
logger.warning('Failed to emit user.created event', error=error) logger.warning('Failed to emit user.created event: %s', error)
return user return user
@@ -1182,7 +1181,7 @@ async def set_email_change_pending(
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
logger.info('Email change pending for user', user_id=user.id, email=user.email, new_email=new_email) logger.info(f'Email change pending for user {user.id}: {user.email} -> {new_email}')
return user return user
@@ -1236,7 +1235,7 @@ async def verify_and_apply_email_change(db: AsyncSession, user: User, code: str)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
logger.info('Email changed for user', user_id=user.id, old_email=old_email, new_email=new_email) logger.info(f'Email changed for user {user.id}: {old_email} -> {new_email}')
return True, 'Email changed successfully' return True, 'Email changed successfully'
@@ -1254,7 +1253,7 @@ async def clear_email_change_pending(db: AsyncSession, user: User) -> None:
user.updated_at = datetime.utcnow() user.updated_at = datetime.utcnow()
await db.commit() await db.commit()
logger.info('Email change cancelled for user', user_id=user.id) logger.info(f'Email change cancelled for user {user.id}')
# --- OAuth provider functions --- # --- OAuth provider functions ---
@@ -1287,7 +1286,7 @@ async def set_user_oauth_provider_id(db: AsyncSession, user: User, provider: str
value: str | int = int(provider_id) if provider == 'vk' else provider_id value: str | int = int(provider_id) if provider == 'vk' else provider_id
setattr(user, column_name, value) setattr(user, column_name, value)
user.updated_at = datetime.now(UTC).replace(tzinfo=None) user.updated_at = datetime.now(UTC).replace(tzinfo=None)
logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id) logger.info(f'Linked {provider} (id={provider_id}) to user {user.id}')
async def create_user_by_oauth( async def create_user_by_oauth(
@@ -1333,9 +1332,7 @@ async def create_user_by_oauth(
await db.refresh(user) await db.refresh(user)
user.promo_group = default_group user.promo_group = default_group
logger.info( logger.info(f'Created OAuth user via {provider} (provider_id={provider_id}) with id={user.id}')
'Created OAuth user via (provider_id=) with id', provider=provider, provider_id=provider_id, user_id=user.id
)
try: try:
from app.services.event_emitter import event_emitter from app.services.event_emitter import event_emitter
@@ -1352,6 +1349,6 @@ async def create_user_by_oauth(
db=db, db=db,
) )
except Exception as error: except Exception as error:
logger.warning('Failed to emit user.created event', error=error) logger.warning('Failed to emit user.created event: %s', error)
return user return user
+6 -6
View File
@@ -1,7 +1,7 @@
import logging
import random import random
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -9,7 +9,7 @@ from app.database.models import User, UserMessage
from app.utils.validators import sanitize_html, validate_html_tags from app.utils.validators import sanitize_html, validate_html_tags
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_user_message( async def create_user_message(
@@ -36,7 +36,7 @@ async def create_user_message(
await db.commit() await db.commit()
await db.refresh(message) await db.refresh(message)
logger.info('✅ Создано сообщение ID пользователем', message_id=message.id, created_by=created_by) logger.info(f'✅ Создано сообщение ID {message.id} пользователем {created_by}')
return message return message
@@ -116,7 +116,7 @@ async def update_user_message(
await db.commit() await db.commit()
await db.refresh(message) await db.refresh(message)
logger.info('📝 Обновлено сообщение ID', message_id=message_id) logger.info(f'📝 Обновлено сообщение ID {message_id}')
return message return message
@@ -133,7 +133,7 @@ async def toggle_user_message_status(db: AsyncSession, message_id: int) -> UserM
await db.refresh(message) await db.refresh(message)
status_text = 'активировано' if message.is_active else 'деактивировано' status_text = 'активировано' if message.is_active else 'деактивировано'
logger.info('🔄 Сообщение ID', message_id=message_id, status_text=status_text) logger.info(f'🔄 Сообщение ID {message_id} {status_text}')
return message return message
@@ -147,7 +147,7 @@ async def delete_user_message(db: AsyncSession, message_id: int) -> bool:
await db.delete(message) await db.delete(message)
await db.commit() await db.commit()
logger.info('🗑️ Удалено сообщение ID', message_id=message_id) logger.info(f'🗑️ Удалено сообщение ID {message_id}')
return True return True
+19 -22
View File
@@ -1,8 +1,8 @@
"""CRUD операции для связи пользователей с промогруппами (Many-to-Many).""" """CRUD операции для связи пользователей с промогруппами (Many-to-Many)."""
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import and_, desc, select from sqlalchemy import and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -10,7 +10,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoGroup, User, UserPromoGroup from app.database.models import PromoGroup, User, UserPromoGroup
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def _sync_user_primary_promo_group( async def _sync_user_primary_promo_group(
@@ -39,7 +39,11 @@ async def _sync_user_primary_promo_group(
user.updated_at = datetime.utcnow() user.updated_at = datetime.utcnow()
except Exception as error: except Exception as error:
logger.error('Ошибка синхронизации primary промогруппы пользователя', user_id=user_id, error=error) logger.error(
'Ошибка синхронизации primary промогруппы пользователя %s: %s',
user_id,
error,
)
async def sync_user_primary_promo_group( async def sync_user_primary_promo_group(
@@ -70,7 +74,7 @@ async def add_user_to_promo_group(
# Проверяем существование связи # Проверяем существование связи
existing = await has_user_promo_group(db, user_id, promo_group_id) existing = await has_user_promo_group(db, user_id, promo_group_id)
if existing: if existing:
logger.info('Пользователь уже имеет промогруппу', user_id=user_id, promo_group_id=promo_group_id) logger.info(f'Пользователь {user_id} уже имеет промогруппу {promo_group_id}')
return None return None
# Создаем новую связь # Создаем новую связь
@@ -87,16 +91,11 @@ async def add_user_to_promo_group(
await db.commit() await db.commit()
await db.refresh(user_promo_group) await db.refresh(user_promo_group)
logger.info( logger.info(f'Пользователю {user_id} добавлена промогруппа {promo_group_id} ({assigned_by})')
'Пользователю добавлена промогруппа',
user_id=user_id,
promo_group_id=promo_group_id,
assigned_by=assigned_by,
)
return user_promo_group return user_promo_group
except Exception as error: except Exception as error:
logger.error('Ошибка добавления промогруппы пользователю', error=error) logger.error(f'Ошибка добавления промогруппы пользователю: {error}')
await db.rollback() await db.rollback()
return None return None
@@ -122,9 +121,7 @@ async def remove_user_from_promo_group(db: AsyncSession, user_id: int, promo_gro
user_promo_group = result.scalar_one_or_none() user_promo_group = result.scalar_one_or_none()
if not user_promo_group: if not user_promo_group:
logger.warning( logger.warning(f'Связь пользователя {user_id} с промогруппой {promo_group_id} не найдена')
'Связь пользователя с промогруппой не найдена', user_id=user_id, promo_group_id=promo_group_id
)
return False return False
await db.delete(user_promo_group) await db.delete(user_promo_group)
@@ -134,11 +131,11 @@ async def remove_user_from_promo_group(db: AsyncSession, user_id: int, promo_gro
await db.commit() await db.commit()
logger.info('У пользователя удалена промогруппа', user_id=user_id, promo_group_id=promo_group_id) logger.info(f'У пользователя {user_id} удалена промогруппа {promo_group_id}')
return True return True
except Exception as error: except Exception as error:
logger.error('Ошибка удаления промогруппы у пользователя', error=error) logger.error(f'Ошибка удаления промогруппы у пользователя: {error}')
await db.rollback() await db.rollback()
return False return False
@@ -165,7 +162,7 @@ async def get_user_promo_groups(db: AsyncSession, user_id: int) -> list[UserProm
return list(result.scalars().all()) return list(result.scalars().all())
except Exception as error: except Exception as error:
logger.error('Ошибка получения промогрупп пользователя', user_id=user_id, error=error) logger.error(f'Ошибка получения промогрупп пользователя {user_id}: {error}')
return [] return []
@@ -190,7 +187,7 @@ async def get_primary_user_promo_group(db: AsyncSession, user_id: int) -> PromoG
return user_promo_groups[0].promo_group if user_promo_groups[0].promo_group else None return user_promo_groups[0].promo_group if user_promo_groups[0].promo_group else None
except Exception as error: except Exception as error:
logger.error('Ошибка получения primary промогруппы пользователя', user_id=user_id, error=error) logger.error(f'Ошибка получения primary промогруппы пользователя {user_id}: {error}')
return None return None
@@ -215,7 +212,7 @@ async def has_user_promo_group(db: AsyncSession, user_id: int, promo_group_id: i
return result.scalar_one_or_none() is not None return result.scalar_one_or_none() is not None
except Exception as error: except Exception as error:
logger.error('Ошибка проверки промогруппы пользователя', error=error) logger.error(f'Ошибка проверки промогруппы пользователя: {error}')
return False return False
@@ -235,7 +232,7 @@ async def count_user_promo_groups(db: AsyncSession, user_id: int) -> int:
return len(list(result.scalars().all())) return len(list(result.scalars().all()))
except Exception as error: except Exception as error:
logger.error('Ошибка подсчета промогрупп пользователя', error=error) logger.error(f'Ошибка подсчета промогрупп пользователя: {error}')
return 0 return 0
@@ -267,10 +264,10 @@ async def replace_user_promo_groups(
db.add(user_promo_group) db.add(user_promo_group)
await db.commit() await db.commit()
logger.info('Промогруппы пользователя заменены на', user_id=user_id, promo_group_ids=promo_group_ids) logger.info(f'Промогруппы пользователя {user_id} заменены на {promo_group_ids}')
return True return True
except Exception as error: except Exception as error:
logger.error('Ошибка замены промогрупп пользователя', error=error) logger.error(f'Ошибка замены промогрупп пользователя: {error}')
await db.rollback() await db.rollback()
return False return False
+14 -12
View File
@@ -1,16 +1,16 @@
"""CRUD helpers for WATA payment records.""" """CRUD helpers for WATA payment records."""
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import WataPayment from app.database.models import WataPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_wata_payment( async def create_wata_payment(
@@ -53,11 +53,11 @@ async def create_wata_payment(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Создан Wata платеж # для пользователя : копеек (статус)', 'Создан Wata платеж #%s для пользователя %s: %s копеек (статус %s)',
payment_id=payment.id, payment.id,
user_id=user_id, user_id,
amount_kopeks=amount_kopeks, amount_kopeks,
status=status, status,
) )
return payment return payment
@@ -128,10 +128,10 @@ async def update_wata_payment_status(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Обновлен Wata платеж : статус is_paid', 'Обновлен Wata платеж %s: статус=%s, is_paid=%s',
payment_link_id=payment.payment_link_id, payment.payment_link_id,
payment_status=payment.status, payment.status,
is_paid=payment.is_paid, payment.is_paid,
) )
return payment return payment
@@ -147,7 +147,9 @@ async def link_wata_payment_to_transaction(
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(
'Wata платеж привязан к транзакции', payment_link_id=payment.payment_link_id, transaction_id=transaction_id 'Wata платеж %s привязан к транзакции %s',
payment.payment_link_id,
transaction_id,
) )
return payment return payment
+16 -16
View File
@@ -1,13 +1,13 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import func, select, update from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User, WelcomeText from app.database.models import User, WelcomeText
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
WELCOME_TEXT_KEY = 'welcome_text' WELCOME_TEXT_KEY = 'welcome_text'
@@ -83,7 +83,7 @@ async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
await db.refresh(welcome_text) await db.refresh(welcome_text)
status = 'включен' if welcome_text.is_enabled else 'отключен' status = 'включен' if welcome_text.is_enabled else 'отключен'
logger.info('Приветственный текст администратором', status=status, admin_id=admin_id) logger.info(f'Приветственный текст {status} администратором {admin_id}')
return welcome_text.is_enabled return welcome_text.is_enabled
default_text = await get_current_welcome_text_or_default() default_text = await get_current_welcome_text_or_default()
new_welcome_text = WelcomeText(text_content=default_text, is_active=True, is_enabled=True, created_by=admin_id) new_welcome_text = WelcomeText(text_content=default_text, is_active=True, is_enabled=True, created_by=admin_id)
@@ -92,11 +92,11 @@ async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
await db.commit() await db.commit()
await db.refresh(new_welcome_text) await db.refresh(new_welcome_text)
logger.info('Создан и включен дефолтный приветственный текст администратором', admin_id=admin_id) logger.info(f'Создан и включен дефолтный приветственный текст администратором {admin_id}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка при переключении статуса приветственного текста', error=e) logger.error(f'Ошибка при переключении статуса приветственного текста: {e}')
await db.rollback() await db.rollback()
return False return False
@@ -116,11 +116,11 @@ async def set_welcome_text(db: AsyncSession, text_content: str, admin_id: int) -
await db.commit() await db.commit()
await db.refresh(new_welcome_text) await db.refresh(new_welcome_text)
logger.info('Установлен новый приветственный текст администратором', admin_id=admin_id) logger.info(f'Установлен новый приветственный текст администратором {admin_id}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка при установке приветственного текста', error=e) logger.error(f'Ошибка при установке приветственного текста: {e}')
await db.rollback() await db.rollback()
return False return False
@@ -154,10 +154,10 @@ async def create_welcome_text(
await db.refresh(welcome_text) await db.refresh(welcome_text)
logger.info( logger.info(
'✅ Создан приветственный текст ID (активный включен=)', '✅ Создан приветственный текст ID %s (активный=%s, включен=%s)',
welcome_text_id=welcome_text.id, welcome_text.id,
is_active=welcome_text.is_active, welcome_text.is_active,
is_enabled=welcome_text.is_enabled, welcome_text.is_enabled,
) )
return welcome_text return welcome_text
@@ -188,10 +188,10 @@ async def update_welcome_text(
await db.refresh(welcome_text) await db.refresh(welcome_text)
logger.info( logger.info(
'📝 Обновлен приветственный текст ID (активный включен=)', '📝 Обновлен приветственный текст ID %s (активный=%s, включен=%s)',
welcome_text_id=welcome_text.id, welcome_text.id,
is_active=welcome_text.is_active, welcome_text.is_active,
is_enabled=welcome_text.is_enabled, welcome_text.is_enabled,
) )
return welcome_text return welcome_text
@@ -199,7 +199,7 @@ async def update_welcome_text(
async def delete_welcome_text(db: AsyncSession, welcome_text: WelcomeText) -> None: async def delete_welcome_text(db: AsyncSession, welcome_text: WelcomeText) -> None:
await db.delete(welcome_text) await db.delete(welcome_text)
await db.commit() await db.commit()
logger.info('🗑️ Удален приветственный текст ID', welcome_text_id=welcome_text.id) logger.info('🗑️ Удален приветственный текст ID %s', welcome_text.id)
async def get_current_welcome_text_or_default() -> str: async def get_current_welcome_text_or_default() -> str:
+8 -8
View File
@@ -2,10 +2,10 @@
CRUD операции для колеса удачи (Fortune Wheel). CRUD операции для колеса удачи (Fortune Wheel).
""" """
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import structlog
from sqlalchemy import and_, desc, func, select from sqlalchemy import and_, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -17,7 +17,7 @@ from app.database.models import (
) )
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
# ==================== WHEEL CONFIG ==================== # ==================== WHEEL CONFIG ====================
@@ -68,7 +68,7 @@ async def update_wheel_config(db: AsyncSession, **kwargs) -> WheelConfig:
config.updated_at = datetime.utcnow() config.updated_at = datetime.utcnow()
await db.commit() await db.commit()
await db.refresh(config) await db.refresh(config)
logger.info('🎡 Обновлена конфигурация колеса', kwargs=kwargs) logger.info(f'🎡 Обновлена конфигурация колеса: {kwargs}')
return config return config
@@ -129,7 +129,7 @@ async def create_wheel_prize(
db.add(prize) db.add(prize)
await db.commit() await db.commit()
await db.refresh(prize) await db.refresh(prize)
logger.info('🎁 Создан приз колеса', display_name=display_name, prize_type=prize_type) logger.info(f'🎁 Создан приз колеса: {display_name} ({prize_type})')
return prize return prize
@@ -146,7 +146,7 @@ async def update_wheel_prize(db: AsyncSession, prize_id: int, **kwargs) -> Wheel
prize.updated_at = datetime.utcnow() prize.updated_at = datetime.utcnow()
await db.commit() await db.commit()
await db.refresh(prize) await db.refresh(prize)
logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs) logger.info(f'🎁 Обновлен приз колеса ID={prize_id}: {kwargs}')
return prize return prize
@@ -158,7 +158,7 @@ async def delete_wheel_prize(db: AsyncSession, prize_id: int) -> bool:
await db.delete(prize) await db.delete(prize)
await db.commit() await db.commit()
logger.info('🗑️ Удален приз колеса ID', prize_id=prize_id) logger.info(f'🗑️ Удален приз колеса ID={prize_id}')
return True return True
@@ -170,7 +170,7 @@ async def reorder_wheel_prizes(db: AsyncSession, prize_ids: list[int]) -> bool:
prize.sort_order = index prize.sort_order = index
await db.commit() await db.commit()
logger.info('🔄 Переупорядочены призы колеса', prize_ids=prize_ids) logger.info(f'🔄 Переупорядочены призы колеса: {prize_ids}')
return True return True
@@ -209,7 +209,7 @@ async def create_wheel_spin(
db.add(spin) db.add(spin)
await db.commit() await db.commit()
await db.refresh(spin) await db.refresh(spin)
logger.info('🎰 Создан спин колеса: user_id=, prize', user_id=user_id, prize_display_name=prize_display_name) logger.info(f"🎰 Создан спин колеса: user_id={user_id}, prize='{prize_display_name}'")
return spin return spin
+10 -24
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime from datetime import datetime
import structlog
from sqlalchemy import and_, select, update from sqlalchemy import and_, select, update
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import YooKassaPayment from app.database.models import YooKassaPayment
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
async def create_yookassa_payment( async def create_yookassa_payment(
@@ -46,20 +46,15 @@ async def create_yookassa_payment(
except IntegrityError as e: except IntegrityError as e:
await db.rollback() await db.rollback()
logger.error( logger.error(
'FK violation при создании платежа YooKassa : user_id= не существует в БД', 'FK violation при создании платежа YooKassa %s: user_id=%s не существует в БД: %s',
yookassa_payment_id=yookassa_payment_id, yookassa_payment_id,
user_id=user_id, user_id,
e=e, e,
) )
return None return None
await db.refresh(payment) await db.refresh(payment)
logger.info( logger.info(f'Создан платеж YooKassa: {yookassa_payment_id} на {amount_kopeks / 100}₽ для пользователя {user_id}')
'Создан платеж YooKassa: на ₽ для пользователя',
yookassa_payment_id=yookassa_payment_id,
amount_kopeks=amount_kopeks / 100,
user_id=user_id,
)
return payment return payment
@@ -109,12 +104,7 @@ async def update_yookassa_payment_status(
payment = result.scalar_one_or_none() payment = result.scalar_one_or_none()
if payment: if payment:
logger.info( logger.info(f'Обновлен статус платежа YooKassa {yookassa_payment_id}: {status}, paid={is_paid}')
'Обновлен статус платежа YooKassa , paid',
yookassa_payment_id=yookassa_payment_id,
status=status,
is_paid=is_paid,
)
return payment return payment
@@ -137,11 +127,7 @@ async def link_yookassa_payment_to_transaction(
payment = result.scalar_one_or_none() payment = result.scalar_one_or_none()
if payment: if payment:
logger.info( logger.info(f'Платеж YooKassa {yookassa_payment_id} связан с транзакцией {transaction_id}')
'Платеж YooKassa связан с транзакцией',
yookassa_payment_id=yookassa_payment_id,
transaction_id=transaction_id,
)
return payment return payment
@@ -199,7 +185,7 @@ async def delete_yookassa_payment(db: AsyncSession, yookassa_payment_id: str) ->
if payment: if payment:
await db.delete(payment) await db.delete(payment)
await db.commit() await db.commit()
logger.info('Удален платеж YooKassa', yookassa_payment_id=yookassa_payment_id) logger.info(f'Удален платеж YooKassa: {yookassa_payment_id}')
return True return True
return False return False
+24 -22
View File
@@ -1,11 +1,11 @@
import asyncio import asyncio
import logging
import time import time
from collections.abc import AsyncGenerator, Callable from collections.abc import AsyncGenerator, Callable
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from functools import wraps from functools import wraps
from typing import TypeVar from typing import TypeVar
import structlog
from sqlalchemy import bindparam, event, inspect, text from sqlalchemy import bindparam, event, inspect, text
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.exc import InterfaceError, OperationalError, ProgrammingError from sqlalchemy.exc import InterfaceError, OperationalError, ProgrammingError
@@ -16,7 +16,7 @@ from app.config import settings
from app.database.models import Base from app.database.models import Base
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
T = TypeVar('T') T = TypeVar('T')
@@ -126,16 +126,16 @@ def with_db_retry(
last_exception = e last_exception = e
if attempt < attempts: if attempt < attempts:
logger.warning( logger.warning(
'Ошибка БД (попытка /): . Повтор через сек...', 'Ошибка БД (попытка %d/%d): %s. Повтор через %.1f сек...',
attempt=attempt, attempt,
attempts=attempts, attempts,
e=str(e)[:100], str(e)[:100],
current_delay=current_delay, current_delay,
) )
await asyncio.sleep(current_delay) await asyncio.sleep(current_delay)
current_delay *= backoff current_delay *= backoff
else: else:
logger.error('Ошибка БД: все попыток исчерпаны. Последняя ошибка', attempts=attempts, e=str(e)) logger.error('Ошибка БД: все %d попыток исчерпаны. Последняя ошибка: %s', attempts, str(e))
raise last_exception raise last_exception
@@ -159,7 +159,7 @@ async def execute_with_retry(
except RETRYABLE_EXCEPTIONS as e: except RETRYABLE_EXCEPTIONS as e:
last_exception = e last_exception = e
if attempt < attempts: if attempt < attempts:
logger.warning('SQL retry (попытка /)', attempt=attempt, attempts=attempts, e=str(e)[:100]) logger.warning('SQL retry (попытка %d/%d): %s', attempt, attempts, str(e)[:100])
await asyncio.sleep(delay) await asyncio.sleep(delay)
delay *= 2 delay *= 2
@@ -175,15 +175,15 @@ if settings.DEBUG:
@event.listens_for(Engine, 'before_cursor_execute') @event.listens_for(Engine, 'before_cursor_execute')
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany): def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
conn.info.setdefault('query_start_time', []).append(time.time()) conn.info.setdefault('query_start_time', []).append(time.time())
logger.debug('🔍 Executing query: ...', statement=statement[:100]) logger.debug(f'🔍 Executing query: {statement[:100]}...')
@event.listens_for(Engine, 'after_cursor_execute') @event.listens_for(Engine, 'after_cursor_execute')
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany): def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
total = time.time() - conn.info['query_start_time'].pop(-1) total = time.time() - conn.info['query_start_time'].pop(-1)
if total > 0.1: # Логируем медленные запросы > 100ms if total > 0.1: # Логируем медленные запросы > 100ms
logger.warning('🐌 Slow query (s): ...', total=round(total, 3), statement=statement[:100]) logger.warning(f'🐌 Slow query ({total:.3f}s): {statement[:100]}...')
else: else:
logger.debug('⚡ Query executed in', total=round(total, 3)) logger.debug(f'⚡ Query executed in {total:.3f}s')
# ============================================================================ # ============================================================================
# ADVANCED SESSION MANAGER WITH READ REPLICAS # ADVANCED SESSION MANAGER WITH READ REPLICAS
@@ -201,7 +201,7 @@ def _validate_database_url(url: str | None) -> str | None:
return None return None
# Простая проверка на валидный формат # Простая проверка на валидный формат
if not ('://' in url or url.startswith('sqlite')): if not ('://' in url or url.startswith('sqlite')):
logger.warning('Невалидный DATABASE_URL', url=url[:20]) logger.warning('Невалидный DATABASE_URL: %s', url[:20])
return None return None
return url return url
@@ -234,9 +234,9 @@ class DatabaseManager:
expire_on_commit=False, expire_on_commit=False,
autoflush=False, autoflush=False,
) )
logger.info('Read replica настроена', replica_url=replica_url[:30] + '...') logger.info('Read replica настроена: %s', replica_url[:30] + '...')
except Exception as e: except Exception as e:
logger.error('Не удалось настроить read replica', e=e) logger.error('Не удалось настроить read replica: %s', e)
self.read_replica_engine = None self.read_replica_engine = None
@asynccontextmanager @asynccontextmanager
@@ -276,10 +276,10 @@ class DatabaseManager:
latency = (time.time() - start) * 1000 latency = (time.time() - start) * 1000
status = 'healthy' status = 'healthy'
except TimeoutError: except TimeoutError:
logger.error('Health check таймаут (сек)', timeout=timeout) logger.error('Health check таймаут (%s сек)', timeout)
status = 'timeout' status = 'timeout'
except Exception as e: except Exception as e:
logger.error('Database health check failed', e=e) logger.error('Database health check failed: %s', e)
status = 'unhealthy' status = 'unhealthy'
return { return {
@@ -307,7 +307,7 @@ class DatabaseManager:
except TimeoutError: except TimeoutError:
status = 'timeout' status = 'timeout'
except Exception as e: except Exception as e:
logger.error('Read replica health check failed', e=e) logger.error('Read replica health check failed: %s', e)
return { return {
'status': status, 'status': status,
@@ -440,7 +440,7 @@ async def init_db():
# Продолжаем выполнение, так как основные таблицы могут быть созданы # Продолжаем выполнение, так как основные таблицы могут быть созданы
else: else:
# Для других ошибок пробрасываем исключение # Для других ошибок пробрасываем исключение
logger.error('❌ Ошибка при создании таблиц', error=e) logger.error(f'❌ Ошибка при создании таблиц: {e}')
raise raise
if not IS_SQLITE: if not IS_SQLITE:
@@ -468,19 +468,21 @@ async def init_db():
if not table_exists: if not table_exists:
logger.debug( logger.debug(
'Пропускаем создание индекса : таблица отсутствует', index_sql=index_sql, table_name=table_name 'Пропускаем создание индекса %s: таблица %s отсутствует',
index_sql,
table_name,
) )
continue continue
try: try:
await conn.execute(text(index_sql)) await conn.execute(text(index_sql))
except Exception as e: except Exception as e:
logger.debug('Index creation skipped for', table_name=table_name, e=e) logger.debug('Index creation skipped for %s: %s', table_name, e)
logger.info('База данных успешно инициализирована') logger.info('База данных успешно инициализирована')
health = await db_manager.health_check() health = await db_manager.health_check()
logger.info('Database health', health=health) logger.info('Database health: %s', health)
async def close_db(): async def close_db():
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -4,13 +4,13 @@ Ban System API Client.
Client for interacting with the BedolagaBan monitoring system. Client for interacting with the BedolagaBan monitoring system.
""" """
import logging
from typing import Any from typing import Any
import aiohttp import aiohttp
import structlog
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class BanSystemAPIError(Exception): class BanSystemAPIError(Exception):
@@ -78,7 +78,7 @@ class BanSystemAPI:
response_text = await response.text() response_text = await response.text()
if response.status >= 400: if response.status >= 400:
logger.error('Ban System API error', status=response.status, response_text=response_text) logger.error(f'Ban System API error: {response.status} - {response_text}')
raise BanSystemAPIError( raise BanSystemAPIError(
message=f'API error {response.status}: {response_text}', message=f'API error {response.status}: {response_text}',
status_code=response.status, status_code=response.status,
@@ -93,7 +93,7 @@ class BanSystemAPI:
return {} return {}
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
logger.error('Ban System API connection error', error=e) logger.error(f'Ban System API connection error: {e}')
raise BanSystemAPIError(message=f'Connection error: {e!s}', status_code=None, response_data=None) raise BanSystemAPIError(message=f'Connection error: {e!s}', status_code=None, response_data=None)
except TimeoutError: except TimeoutError:
logger.error('Ban System API request timeout') logger.error('Ban System API request timeout')
+8 -8
View File
@@ -1,14 +1,14 @@
import hashlib import hashlib
import hmac import hmac
import logging
from typing import Any from typing import Any
import aiohttp import aiohttp
import structlog
from app.config import settings from app.config import settings
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class CryptoBotService: class CryptoBotService:
@@ -49,11 +49,11 @@ class CryptoBotService:
if response.status == 200 and response_data.get('ok'): if response.status == 200 and response_data.get('ok'):
return response_data.get('result') return response_data.get('result')
logger.error('CryptoBot API ошибка', response_data=response_data) logger.error(f'CryptoBot API ошибка: {response_data}')
return None return None
except Exception as e: except Exception as e:
logger.error('Ошибка запроса к CryptoBot API', error=e) logger.error(f'Ошибка запроса к CryptoBot API: {e}')
return None return None
async def get_me(self) -> dict[str, Any] | None: async def get_me(self) -> dict[str, Any] | None:
@@ -81,7 +81,7 @@ class CryptoBotService:
result = await self._make_request('POST', 'createInvoice', data) result = await self._make_request('POST', 'createInvoice', data)
if result: if result:
logger.info('Создан CryptoBot invoice на', get=result.get('invoice_id'), amount=amount, asset=asset) logger.info(f'Создан CryptoBot invoice {result.get("invoice_id")} на {amount} {asset}')
return result return result
@@ -140,7 +140,7 @@ class CryptoBotService:
return is_valid return is_valid
except Exception as e: except Exception as e:
logger.error('Ошибка проверки подписи CryptoBot webhook', error=e) logger.error(f'Ошибка проверки подписи CryptoBot webhook: {e}')
return False return False
async def process_webhook(self, webhook_data: dict[str, Any]) -> dict[str, Any] | None: async def process_webhook(self, webhook_data: dict[str, Any]) -> dict[str, Any] | None:
@@ -161,9 +161,9 @@ class CryptoBotService:
'payment_system': 'cryptobot', 'payment_system': 'cryptobot',
} }
logger.warning('Неизвестный тип CryptoBot webhook', update_type=update_type) logger.warning(f'Неизвестный тип CryptoBot webhook: {update_type}')
return None return None
except Exception as e: except Exception as e:
logger.error('Ошибка обработки CryptoBot webhook', error=e) logger.error(f'Ошибка обработки CryptoBot webhook: {e}')
return None return None
+9 -13
View File
@@ -6,15 +6,15 @@ import base64
import hashlib import hashlib
import hmac import hmac
import json import json
import logging
from typing import Any from typing import Any
import aiohttp import aiohttp
import structlog
from app.config import settings from app.config import settings
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class HeleketService: class HeleketService:
@@ -93,28 +93,26 @@ class HeleketService:
): ):
text = await response.text() text = await response.text()
if response.content_type != 'application/json': if response.content_type != 'application/json':
logger.error('Ответ Heleket не JSON', content_type=response.content_type, text=text) logger.error('Ответ Heleket не JSON (%s): %s', response.content_type, text)
return None return None
try: try:
data = json.loads(text) data = json.loads(text)
except json.JSONDecodeError: except json.JSONDecodeError:
logger.error('Ошибка парсинга Heleket JSON', text=text) logger.error('Ошибка парсинга Heleket JSON: %s', text)
return None return None
if response.status >= 400: if response.status >= 400:
logger.error( logger.error('Heleket API %s вернул статус %s: %s', endpoint, response.status, data)
'Heleket API вернул статус', endpoint=endpoint, response_status=response.status, data=data
)
return None return None
if isinstance(data, dict) and data.get('state') == 0: if isinstance(data, dict) and data.get('state') == 0:
return data return data
logger.error('Heleket API вернул ошибку', data=data) logger.error('Heleket API вернул ошибку: %s', data)
return None return None
except Exception as error: # pragma: no cover - defensive except Exception as error: # pragma: no cover - defensive
logger.error('Ошибка запроса к Heleket API', error=error) logger.error('Ошибка запроса к Heleket API: %s', error)
return None return None
async def create_payment(self, payload: dict[str, Any]) -> dict[str, Any] | None: async def create_payment(self, payload: dict[str, Any]) -> dict[str, Any] | None:
@@ -159,7 +157,7 @@ class HeleketService:
return True return True
if not isinstance(payload, dict): if not isinstance(payload, dict):
logger.error('Heleket webhook payload не dict', payload=payload) logger.error('Heleket webhook payload не dict: %s', payload)
return False return False
signature = payload.get('sign') signature = payload.get('sign')
@@ -175,7 +173,5 @@ class HeleketService:
is_valid = hmac.compare_digest(expected, str(signature)) is_valid = hmac.compare_digest(expected, str(signature))
if not is_valid: if not is_valid:
logger.error( logger.error('Неверная подпись Heleket webhook: ожидается %s, получено %s', expected, signature)
'Неверная подпись Heleket webhook: ожидается , получено', expected=expected, signature=signature
)
return is_valid return is_valid
+10 -10
View File
@@ -1,8 +1,8 @@
import asyncio import asyncio
import json import json
import logging
from typing import Any from typing import Any
import structlog
from aiohttp import web from aiohttp import web
from app.config import settings from app.config import settings
@@ -11,7 +11,7 @@ from app.external.heleket import HeleketService
from app.services.payment_service import PaymentService from app.services.payment_service import PaymentService
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class HeleketWebhookHandler: class HeleketWebhookHandler:
@@ -39,7 +39,7 @@ class HeleketWebhookHandler:
processed = await self.payment_service.process_heleket_webhook(db, payload) processed = await self.payment_service.process_heleket_webhook(db, payload)
await db.commit() await db.commit()
except Exception as e: except Exception as e:
logger.error('Ошибка обработки Heleket webhook', error=e) logger.error(f'Ошибка обработки Heleket webhook: {e}')
await db.rollback() await db.rollback()
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500) return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
@@ -97,15 +97,15 @@ async def start_heleket_webhook_server(payment_service: PaymentService) -> None:
try: try:
await site.start() await site.start()
logger.info( logger.info(
'Heleket webhook сервер запущен на', 'Heleket webhook сервер запущен на %s:%s',
HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST, settings.HELEKET_WEBHOOK_HOST,
HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT, settings.HELEKET_WEBHOOK_PORT,
) )
logger.info( logger.info(
'Heleket webhook URL: http://', 'Heleket webhook URL: http://%s:%s%s',
HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST, settings.HELEKET_WEBHOOK_HOST,
HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT, settings.HELEKET_WEBHOOK_PORT,
HELEKET_WEBHOOK_PATH=settings.HELEKET_WEBHOOK_PATH, settings.HELEKET_WEBHOOK_PATH,
) )
while True: while True:
+15 -6
View File
@@ -3,17 +3,17 @@
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import logging
from dataclasses import dataclass from dataclasses import dataclass
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from typing import Any from typing import Any
import aiohttp import aiohttp
import structlog
from app.config import settings from app.config import settings
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class Pal24APIError(Exception): class Pal24APIError(Exception):
@@ -96,21 +96,30 @@ class Pal24Client:
payload = await response.json(content_type=None) payload = await response.json(content_type=None)
except aiohttp.ContentTypeError: except aiohttp.ContentTypeError:
text_body = await response.text() text_body = await response.text()
logger.error('Pal24 API returned non-JSON response for', endpoint=endpoint, text_body=text_body) logger.error(
'Pal24 API returned non-JSON response for %s: %s',
endpoint,
text_body,
)
raise Pal24APIError(f'Pal24 API returned non-JSON response: {text_body}') from None raise Pal24APIError(f'Pal24 API returned non-JSON response: {text_body}') from None
result = Pal24Response.from_payload(payload, status) result = Pal24Response.from_payload(payload, status)
if status >= 400 or not result.success: if status >= 400 or not result.success:
logger.error('Pal24 API error', status=status, endpoint=endpoint, payload=payload) logger.error(
'Pal24 API error %s %s: %s',
status,
endpoint,
payload,
)
result.raise_for_status(endpoint) result.raise_for_status(endpoint)
return result return result
except TimeoutError as error: except TimeoutError as error:
logger.error('Pal24 API request timeout for', endpoint=endpoint, error=error) logger.error('Pal24 API request timeout for %s: %s', endpoint, error)
raise Pal24APIError(f'Pal24 API request timeout for {endpoint}') from error raise Pal24APIError(f'Pal24 API request timeout for {endpoint}') from error
except aiohttp.ClientError as error: except aiohttp.ClientError as error:
logger.error('Pal24 API client error for', endpoint=endpoint, error=error) logger.error('Pal24 API client error for %s: %s', endpoint, error)
raise Pal24APIError(str(error)) from error raise Pal24APIError(str(error)) from error
# API methods ----------------------------------------------------------------- # API methods -----------------------------------------------------------------
+28 -28
View File
@@ -1,6 +1,7 @@
import asyncio import asyncio
import base64 import base64
import json import json
import logging
import ssl import ssl
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@@ -9,10 +10,9 @@ from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
import aiohttp import aiohttp
import structlog
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class UserStatus(Enum): class UserStatus(Enum):
@@ -310,7 +310,7 @@ class RemnaWaveAPI:
async def __aenter__(self): async def __aenter__(self):
conn_type = self._detect_connection_type() conn_type = self._detect_connection_type()
logger.debug('Подключение к Remnawave: (тип: )', base_url=self.base_url, conn_type=conn_type) logger.debug(f'Подключение к Remnawave: {self.base_url} (тип: {conn_type})')
headers = self._prepare_auth_headers() headers = self._prepare_auth_headers()
@@ -319,10 +319,10 @@ class RemnaWaveAPI:
if ':' in self.secret_key: if ':' in self.secret_key:
key_name, key_value = self.secret_key.split(':', 1) key_name, key_value = self.secret_key.split(':', 1)
cookies = {key_name: key_value} cookies = {key_name: key_value}
logger.debug('Используем куки: =***', key_name=key_name) logger.debug(f'Используем куки: {key_name}=***')
else: else:
cookies = {self.secret_key: self.secret_key} cookies = {self.secret_key: self.secret_key}
logger.debug('Используем куки: =***', secret_key=self.secret_key) logger.debug(f'Используем куки: {self.secret_key}=***')
connector_kwargs = {} connector_kwargs = {}
@@ -388,12 +388,12 @@ class RemnaWaveAPI:
if response.status == 429 and attempt < max_retries: if response.status == 429 and attempt < max_retries:
retry_after = float(response.headers.get('Retry-After', base_delay * (2**attempt))) retry_after = float(response.headers.get('Retry-After', base_delay * (2**attempt)))
logger.warning( logger.warning(
'Rate limited (429) on , retry / after s', 'Rate limited (429) on %s %s, retry %d/%d after %.1fs',
method=method, method,
endpoint=endpoint, endpoint,
attempt=attempt + 1, attempt + 1,
max_retries=max_retries, max_retries,
retry_after=retry_after, retry_after,
) )
await asyncio.sleep(retry_after) await asyncio.sleep(retry_after)
continue continue
@@ -411,17 +411,17 @@ class RemnaWaveAPI:
if attempt < max_retries: if attempt < max_retries:
delay = base_delay * (2**attempt) delay = base_delay * (2**attempt)
logger.warning( logger.warning(
'Request failed on retry / after s', 'Request failed on %s %s: %s, retry %d/%d after %.1fs',
method=method, method,
endpoint=endpoint, endpoint,
e=e, e,
attempt=attempt + 1, attempt + 1,
max_retries=max_retries, max_retries,
delay=delay, delay,
) )
await asyncio.sleep(delay) await asyncio.sleep(delay)
continue continue
logger.error('Request failed', error=e) logger.error(f'Request failed: {e}')
raise RemnaWaveAPIError(f'Request failed: {e!s}') raise RemnaWaveAPIError(f'Request failed: {e!s}')
raise RemnaWaveAPIError(f'Max retries exceeded for {method} {endpoint}') raise RemnaWaveAPIError(f'Max retries exceeded for {method} {endpoint}')
@@ -461,7 +461,7 @@ class RemnaWaveAPI:
if active_internal_squads: if active_internal_squads:
data['activeInternalSquads'] = active_internal_squads data['activeInternalSquads'] = active_internal_squads
logger.debug('Создание пользователя в панели', data=data) logger.debug('Создание пользователя в панели: %s', data)
response = await self._make_request('POST', '/api/users', data) response = await self._make_request('POST', '/api/users', data)
user = self._parse_user(response['response']) user = self._parse_user(response['response'])
return await self.enrich_user_with_happ_link(user) return await self.enrich_user_with_happ_link(user)
@@ -1049,13 +1049,13 @@ class RemnaWaveAPI:
delete_data = {'userUuid': user_uuid, 'hwid': device_hwid} delete_data = {'userUuid': user_uuid, 'hwid': device_hwid}
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data) await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
except Exception as device_error: except Exception as device_error:
logger.error('Ошибка удаления устройства', device_hwid=device_hwid, device_error=device_error) logger.error(f'Ошибка удаления устройства {device_hwid}: {device_error}')
failed_count += 1 failed_count += 1
return failed_count < len(devices) / 2 return failed_count < len(devices) / 2
except Exception as e: except Exception as e:
logger.error('Ошибка при сбросе устройств', error=e) logger.error(f'Ошибка при сбросе устройств: {e}')
return False return False
async def remove_device(self, user_uuid: str, device_hwid: str) -> bool: async def remove_device(self, user_uuid: str, device_hwid: str) -> bool:
@@ -1064,7 +1064,7 @@ class RemnaWaveAPI:
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data) await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка удаления устройства', device_hwid=device_hwid, error=e) logger.error(f'Ошибка удаления устройства {device_hwid}: {e}')
return False return False
async def encrypt_happ_crypto_link(self, link_to_encrypt: str) -> str | None: async def encrypt_happ_crypto_link(self, link_to_encrypt: str) -> str | None:
@@ -1073,10 +1073,10 @@ class RemnaWaveAPI:
response = await self._make_request('POST', '/api/system/tools/happ/encrypt', data) response = await self._make_request('POST', '/api/system/tools/happ/encrypt', data)
return response.get('response', {}).get('encryptedLink') return response.get('response', {}).get('encryptedLink')
except RemnaWaveAPIError as e: except RemnaWaveAPIError as e:
logger.warning('Не удалось зашифровать happ ссылку', message=e.message) logger.warning(f'Не удалось зашифровать happ ссылку: {e.message}')
return None return None
except Exception as e: except Exception as e:
logger.warning('Ошибка при шифровании happ ссылки', error=e) logger.warning(f'Ошибка при шифровании happ ссылки: {e}')
return None return None
async def enrich_user_with_happ_link(self, user: RemnaWaveUser) -> RemnaWaveUser: async def enrich_user_with_happ_link(self, user: RemnaWaveUser) -> RemnaWaveUser:
@@ -1112,7 +1112,7 @@ class RemnaWaveAPI:
try: try:
status = UserStatus(status_str) status = UserStatus(status_str)
except ValueError: except ValueError:
logger.warning('Неизвестный статус пользователя: используем ACTIVE', status_str=status_str) logger.warning(f'Неизвестный статус пользователя: {status_str}, используем ACTIVE')
status = UserStatus.ACTIVE status = UserStatus.ACTIVE
# Получаем trafficLimitStrategy с fallback # Получаем trafficLimitStrategy с fallback
@@ -1120,7 +1120,7 @@ class RemnaWaveAPI:
try: try:
traffic_strategy = TrafficLimitStrategy(strategy_str) traffic_strategy = TrafficLimitStrategy(strategy_str)
except ValueError: except ValueError:
logger.warning('Неизвестная стратегия трафика: используем NO_RESET', strategy_str=strategy_str) logger.warning(f'Неизвестная стратегия трафика: {strategy_str}, используем NO_RESET')
traffic_strategy = TrafficLimitStrategy.NO_RESET traffic_strategy = TrafficLimitStrategy.NO_RESET
return RemnaWaveUser( return RemnaWaveUser(
@@ -1285,5 +1285,5 @@ async def test_api_connection(api: RemnaWaveAPI) -> bool:
await api.get_system_stats() await api.get_system_stats()
return True return True
except Exception as e: except Exception as e:
logger.warning('API connection test failed', e=e) logger.warning('API connection test failed: %s', e)
return False return False
+10 -16
View File
@@ -1,14 +1,14 @@
import logging
from decimal import ROUND_HALF_UP, Decimal from decimal import ROUND_HALF_UP, Decimal
from typing import Any from typing import Any
import structlog
from aiogram import Bot from aiogram import Bot
from aiogram.types import InlineKeyboardMarkup, LabeledPrice from aiogram.types import InlineKeyboardMarkup, LabeledPrice
from app.config import settings from app.config import settings
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class TelegramStarsService: class TelegramStarsService:
@@ -49,16 +49,13 @@ class TelegramStarsService:
) )
logger.info( logger.info(
'Создан Stars invoice на звезд (~) для курс: ₽/⭐', f'Создан Stars invoice на {stars_amount} звезд (~{settings.format_price(amount_kopeks)}) '
stars_amount=stars_amount, f'для {chat_id}, курс: {stars_rate}₽/⭐'
settings=settings.format_price(amount_kopeks),
chat_id=chat_id,
stars_rate=stars_rate,
) )
return invoice_link return invoice_link
except Exception as e: except Exception as e:
logger.error('Ошибка создания Stars invoice', error=e) logger.error(f'Ошибка создания Stars invoice: {e}')
return None return None
async def send_invoice( async def send_invoice(
@@ -87,11 +84,8 @@ class TelegramStarsService:
) )
logger.info( logger.info(
'Отправлен Stars invoice на звезд (~), курс: ₽/⭐', f'Отправлен Stars invoice {message.message_id} на {stars_amount} звезд '
message_id=message.message_id, f'(~{settings.format_price(amount_kopeks)}), курс: {stars_rate}₽/⭐'
stars_amount=stars_amount,
settings=settings.format_price(amount_kopeks),
stars_rate=stars_rate,
) )
return { return {
'message_id': message.message_id, 'message_id': message.message_id,
@@ -101,7 +95,7 @@ class TelegramStarsService:
} }
except Exception as e: except Exception as e:
logger.error('Ошибка отправки Stars invoice', error=e) logger.error(f'Ошибка отправки Stars invoice: {e}')
return None return None
async def answer_pre_checkout_query( async def answer_pre_checkout_query(
@@ -111,8 +105,8 @@ class TelegramStarsService:
await self.bot.answer_pre_checkout_query( await self.bot.answer_pre_checkout_query(
pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message
) )
logger.info('Ответ на pre_checkout_query: ok', ok=ok) logger.info(f'Ответ на pre_checkout_query: ok={ok}')
return True return True
except Exception as e: except Exception as e:
logger.error('Ошибка ответа на pre_checkout_query', error=e) logger.error(f'Ошибка ответа на pre_checkout_query: {e}')
return False return False
+17 -24
View File
@@ -1,14 +1,13 @@
import hashlib import hashlib
import hmac import hmac
import json import json
import logging
from typing import Any from typing import Any
import structlog
from app.config import settings from app.config import settings
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class TributeService: class TributeService:
@@ -26,11 +25,11 @@ class TributeService:
try: try:
payment_url = f'{self.donate_link}&user_id={user_id}' payment_url = f'{self.donate_link}&user_id={user_id}'
logger.info('Создана ссылка Tribute для пользователя', user_id=user_id) logger.info(f'Создана ссылка Tribute для пользователя {user_id}')
return payment_url return payment_url
except Exception as e: except Exception as e:
logger.error('Ошибка создания Tribute ссылки', error=e) logger.error(f'Ошибка создания Tribute ссылки: {e}')
return None return None
def verify_webhook_signature(self, payload: str, signature: str) -> bool: def verify_webhook_signature(self, payload: str, signature: str) -> bool:
@@ -51,7 +50,7 @@ class TributeService:
return is_valid return is_valid
except Exception as e: except Exception as e:
logger.error('Ошибка проверки подписи webhook', error=e) logger.error(f'Ошибка проверки подписи webhook: {e}')
return False return False
async def process_webhook(self, payload_or_data) -> dict[str, Any] | None: async def process_webhook(self, payload_or_data) -> dict[str, Any] | None:
@@ -61,9 +60,9 @@ class TributeService:
if isinstance(payload_or_data, str): if isinstance(payload_or_data, str):
try: try:
webhook_data = json.loads(payload_or_data) webhook_data = json.loads(payload_or_data)
logger.info('📊 Распарсенные данные', webhook_data=webhook_data) logger.info(f'📊 Распарсенные данные: {webhook_data}')
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error('❌ Ошибка парсинга JSON', error=e) logger.error(f'❌ Ошибка парсинга JSON: {e}')
return None return None
else: else:
webhook_data = payload_or_data webhook_data = payload_or_data
@@ -100,24 +99,18 @@ class TributeService:
status = 'unknown' status = 'unknown'
logger.info( logger.info(
'📝 Извлеченные данные: payment_id=, status=, amount_kopeks=, user_id', f'📝 Извлеченные данные: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}'
payment_id=payment_id,
status=status,
amount_kopeks=amount_kopeks,
telegram_user_id=telegram_user_id,
) )
if not telegram_user_id: if not telegram_user_id:
logger.error('❌ Не найден telegram_user_id в webhook данных') logger.error('❌ Не найден telegram_user_id в webhook данных')
logger.error( logger.error(f'🔍 Полные данные для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}')
'🔍 Полные данные для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2)
)
return None return None
try: try:
telegram_user_id = int(telegram_user_id) telegram_user_id = int(telegram_user_id)
except (ValueError, TypeError): except (ValueError, TypeError):
logger.error('❌ Некорректный telegram_user_id', telegram_user_id=telegram_user_id) logger.error(f'❌ Некорректный telegram_user_id: {telegram_user_id}')
return None return None
result = { result = {
@@ -130,28 +123,28 @@ class TributeService:
'payment_system': 'tribute', 'payment_system': 'tribute',
} }
logger.info('✅ Tribute webhook обработан успешно', result=result) logger.info(f'✅ Tribute webhook обработан успешно: {result}')
return result return result
except Exception as e: except Exception as e:
logger.error('❌ Ошибка обработки Tribute webhook', error=e, exc_info=True) logger.error(f'❌ Ошибка обработки Tribute webhook: {e}', exc_info=True)
logger.error('🔍 Webhook data для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2)) logger.error(f'🔍 Webhook data для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}')
return None return None
async def get_payment_status(self, payment_id: str) -> dict[str, Any] | None: async def get_payment_status(self, payment_id: str) -> dict[str, Any] | None:
try: try:
logger.info('Запрос статуса платежа', payment_id=payment_id) logger.info(f'Запрос статуса платежа {payment_id}')
return {'status': 'unknown', 'payment_id': payment_id} return {'status': 'unknown', 'payment_id': payment_id}
except Exception as e: except Exception as e:
logger.error('Ошибка получения статуса платежа', error=e) logger.error(f'Ошибка получения статуса платежа: {e}')
return None return None
async def refund_payment( async def refund_payment(
self, payment_id: str, amount_kopeks: int | None = None, reason: str = 'Возврат по запросу' self, payment_id: str, amount_kopeks: int | None = None, reason: str = 'Возврат по запросу'
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
try: try:
logger.info('Создание возврата для платежа', payment_id=payment_id) logger.info(f'Создание возврата для платежа {payment_id}')
return {'refund_id': f'refund_{payment_id}', 'status': 'pending'} return {'refund_id': f'refund_{payment_id}', 'status': 'pending'}
except Exception as e: except Exception as e:
logger.error('Ошибка создания возврата', error=e) logger.error(f'Ошибка создания возврата: {e}')
return None return None
+28 -21
View File
@@ -3,11 +3,11 @@ from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import json import json
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
import aiohttp import aiohttp
import structlog
from aiohttp import web from aiohttp import web
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
@@ -18,7 +18,7 @@ from app.database.database import AsyncSessionLocal
from app.services.payment_service import PaymentService from app.services.payment_service import PaymentService
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class WataPublicKeyProvider: class WataPublicKeyProvider:
@@ -67,24 +67,28 @@ class WataPublicKeyProvider:
async with aiohttp.ClientSession(timeout=timeout) as session, session.get(url) as response: async with aiohttp.ClientSession(timeout=timeout) as session, session.get(url) as response:
text = await response.text() text = await response.text()
if response.status >= 400: if response.status >= 400:
logger.error('Ошибка получения публичного ключа WATA', response_status=response.status, text=text) logger.error(
'Ошибка получения публичного ключа WATA %s: %s',
response.status,
text,
)
return None return None
try: try:
payload = await response.json() payload = await response.json()
except aiohttp.ContentTypeError: except aiohttp.ContentTypeError:
logger.error('Ответ WATA public-key не является JSON', text=text) logger.error('Ответ WATA public-key не является JSON: %s', text)
return None return None
if isinstance(payload, dict): if isinstance(payload, dict):
value = payload.get('value') value = payload.get('value')
if value: if value:
return value return value
logger.error('Ответ WATA public-key не содержит ключ', payload=payload) logger.error('Ответ WATA public-key не содержит ключ: %s', payload)
else: else:
logger.error('Неожиданный формат ответа WATA public-key', payload=payload) logger.error('Неожиданный формат ответа WATA public-key: %s', payload)
except Exception as error: except Exception as error:
logger.error('Ошибка запроса публичного ключа WATA', error=error) logger.error('Ошибка запроса публичного ключа WATA: %s', error)
return None return None
@@ -121,7 +125,7 @@ class WataWebhookHandler:
try: try:
public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8')) public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
except ValueError as error: except ValueError as error:
logger.error('Ошибка загрузки публичного ключа WATA', error=error) logger.error('Ошибка загрузки публичного ключа WATA: %s', error)
return False return False
try: try:
@@ -136,7 +140,7 @@ class WataWebhookHandler:
logger.warning('Подпись WATA webhook не прошла проверку') logger.warning('Подпись WATA webhook не прошла проверку')
return False return False
except Exception as error: except Exception as error:
logger.error('Ошибка проверки подписи WATA', error=error) logger.error('Ошибка проверки подписи WATA: %s', error)
return False return False
async def handle_webhook(self, request: web.Request) -> web.Response: async def handle_webhook(self, request: web.Request) -> web.Response:
@@ -160,9 +164,9 @@ class WataWebhookHandler:
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400) return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
logger.info( logger.info(
'Получен WATA webhook: order_id status', 'Получен WATA webhook: order_id=%s, status=%s',
payload=payload.get('orderId'), payload.get('orderId'),
payload_2=payload.get('transactionStatus'), payload.get('transactionStatus'),
) )
processed: bool | None = None processed: bool | None = None
@@ -171,7 +175,7 @@ class WataWebhookHandler:
processed = await self.payment_service.process_wata_webhook(db, payload) processed = await self.payment_service.process_wata_webhook(db, payload)
await db.commit() await db.commit()
except Exception as e: except Exception as e:
logger.error('Ошибка обработки WATA webhook', error=e) logger.error(f'Ошибка обработки WATA webhook: {e}')
await db.rollback() await db.rollback()
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500) return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
@@ -214,7 +218,10 @@ def create_wata_webhook_app(payment_service: PaymentService) -> web.Application:
app.router.add_options(settings.WATA_WEBHOOK_PATH, handler.options_handler) app.router.add_options(settings.WATA_WEBHOOK_PATH, handler.options_handler)
app.router.add_get('/health', handler.health_check) app.router.add_get('/health', handler.health_check)
logger.info('Настроен WATA webhook endpoint на', WATA_WEBHOOK_PATH=settings.WATA_WEBHOOK_PATH) logger.info(
'Настроен WATA webhook endpoint на %s',
settings.WATA_WEBHOOK_PATH,
)
return app return app
@@ -237,15 +244,15 @@ async def start_wata_webhook_server(payment_service: PaymentService) -> None:
try: try:
await site.start() await site.start()
logger.info( logger.info(
'WATA webhook сервер запущен на', 'WATA webhook сервер запущен на %s:%s',
WATA_WEBHOOK_HOST=settings.WATA_WEBHOOK_HOST, settings.WATA_WEBHOOK_HOST,
WATA_WEBHOOK_PORT=settings.WATA_WEBHOOK_PORT, settings.WATA_WEBHOOK_PORT,
) )
logger.info( logger.info(
'WATA webhook URL: http://', 'WATA webhook URL: http://%s:%s%s',
WATA_WEBHOOK_HOST=settings.WATA_WEBHOOK_HOST, settings.WATA_WEBHOOK_HOST,
WATA_WEBHOOK_PORT=settings.WATA_WEBHOOK_PORT, settings.WATA_WEBHOOK_PORT,
WATA_WEBHOOK_PATH=settings.WATA_WEBHOOK_PATH, settings.WATA_WEBHOOK_PATH,
) )
while True: while True:
+79 -62
View File
@@ -2,9 +2,9 @@ import base64
import hashlib import hashlib
import hmac import hmac
import json import json
import logging
from collections.abc import Iterable from collections.abc import Iterable
import structlog
from aiogram import Bot from aiogram import Bot
from aiohttp import web from aiohttp import web
@@ -14,7 +14,7 @@ from app.services.payment_service import PaymentService
from app.services.tribute_service import TributeService from app.services.tribute_service import TributeService
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
class WebhookServer: class WebhookServer:
@@ -51,8 +51,8 @@ class WebhookServer:
missing.append('FREEKASSA_SECRET_WORD_2') missing.append('FREEKASSA_SECRET_WORD_2')
if missing: if missing:
logger.warning( logger.warning(
'Freekassa ENABLED=true, но webhook не зарегистрирован. Отсутствуют параметры', f'Freekassa ENABLED=true, но webhook не зарегистрирован. '
value=', '.join(missing), f'Отсутствуют параметры: {", ".join(missing)}'
) )
self.app.router.add_get('/health', self._health_check) self.app.router.add_get('/health', self._health_check)
@@ -66,16 +66,18 @@ class WebhookServer:
self.app.router.add_options(settings.FREEKASSA_WEBHOOK_PATH, self._options_handler) self.app.router.add_options(settings.FREEKASSA_WEBHOOK_PATH, self._options_handler)
logger.info('Webhook сервер настроен:') logger.info('Webhook сервер настроен:')
logger.info('Tribute webhook: POST', TRIBUTE_WEBHOOK_PATH=settings.TRIBUTE_WEBHOOK_PATH) logger.info(f' - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}')
if settings.is_mulenpay_enabled(): if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name() mulenpay_name = settings.get_mulenpay_display_name()
logger.info( logger.info(
'- webhook: POST', mulenpay_name=mulenpay_name, MULENPAY_WEBHOOK_PATH=settings.MULENPAY_WEBHOOK_PATH ' - %s webhook: POST %s',
mulenpay_name,
settings.MULENPAY_WEBHOOK_PATH,
) )
if settings.is_cryptobot_enabled(): if settings.is_cryptobot_enabled():
logger.info('CryptoBot webhook: POST', CRYPTOBOT_WEBHOOK_PATH=settings.CRYPTOBOT_WEBHOOK_PATH) logger.info(f' - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}')
if settings.is_freekassa_enabled(): if settings.is_freekassa_enabled():
logger.info('Freekassa webhook: POST', FREEKASSA_WEBHOOK_PATH=settings.FREEKASSA_WEBHOOK_PATH) logger.info(f' - Freekassa webhook: POST {settings.FREEKASSA_WEBHOOK_PATH}')
logger.info(' - Health check: GET /health') logger.info(' - Health check: GET /health')
return self.app return self.app
@@ -93,35 +95,35 @@ class WebhookServer:
await self.site.start() await self.site.start()
logger.info( logger.info(
'Webhook сервер запущен на', 'Webhook сервер запущен на %s:%s',
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST, settings.TRIBUTE_WEBHOOK_HOST,
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT, settings.TRIBUTE_WEBHOOK_PORT,
) )
logger.info( logger.info(
'Tribute webhook URL: http://', 'Tribute webhook URL: http://%s:%s%s',
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST, settings.TRIBUTE_WEBHOOK_HOST,
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT, settings.TRIBUTE_WEBHOOK_PORT,
TRIBUTE_WEBHOOK_PATH=settings.TRIBUTE_WEBHOOK_PATH, settings.TRIBUTE_WEBHOOK_PATH,
) )
if settings.is_mulenpay_enabled(): if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name() mulenpay_name = settings.get_mulenpay_display_name()
logger.info( logger.info(
'webhook URL: http://', '%s webhook URL: http://%s:%s%s',
mulenpay_name=mulenpay_name, mulenpay_name,
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST, settings.TRIBUTE_WEBHOOK_HOST,
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT, settings.TRIBUTE_WEBHOOK_PORT,
MULENPAY_WEBHOOK_PATH=settings.MULENPAY_WEBHOOK_PATH, settings.MULENPAY_WEBHOOK_PATH,
) )
if settings.is_cryptobot_enabled(): if settings.is_cryptobot_enabled():
logger.info( logger.info(
'CryptoBot webhook URL: http://', 'CryptoBot webhook URL: http://%s:%s%s',
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST, settings.TRIBUTE_WEBHOOK_HOST,
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT, settings.TRIBUTE_WEBHOOK_PORT,
CRYPTOBOT_WEBHOOK_PATH=settings.CRYPTOBOT_WEBHOOK_PATH, settings.CRYPTOBOT_WEBHOOK_PATH,
) )
except Exception as e: except Exception as e:
logger.error('Ошибка запуска webhook сервера', error=e) logger.error(f'Ошибка запуска webhook сервера: {e}')
raise raise
async def stop(self): async def stop(self):
@@ -135,7 +137,7 @@ class WebhookServer:
logger.info('Webhook runner очищен') logger.info('Webhook runner очищен')
except Exception as e: except Exception as e:
logger.error('Ошибка остановки webhook сервера', error=e) logger.error(f'Ошибка остановки webhook сервера: {e}')
async def _options_handler(self, request: web.Request) -> web.Response: async def _options_handler(self, request: web.Request) -> web.Response:
return web.Response( return web.Response(
@@ -150,27 +152,36 @@ class WebhookServer:
async def _mulenpay_webhook_handler(self, request: web.Request) -> web.Response: async def _mulenpay_webhook_handler(self, request: web.Request) -> web.Response:
try: try:
mulenpay_name = settings.get_mulenpay_display_name() mulenpay_name = settings.get_mulenpay_display_name()
logger.info('webhook', mulenpay_name=mulenpay_name, method=request.method, request_path=request.path) logger.info(
logger.info('webhook headers', mulenpay_name=mulenpay_name, headers=dict(request.headers)) '%s webhook: %s %s',
mulenpay_name,
request.method,
request.path,
)
logger.info(
'%s webhook headers: %s',
mulenpay_name,
dict(request.headers),
)
raw_body = await request.read() raw_body = await request.read()
if not raw_body: if not raw_body:
logger.warning('Пустой webhook', mulenpay_name=mulenpay_name) logger.warning('Пустой %s webhook', mulenpay_name)
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400) return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
# Временно отключаем проверку подписи для отладки # Временно отключаем проверку подписи для отладки
# TODO: Включить обратно после настройки MulenPay # TODO: Включить обратно после настройки MulenPay
if not self._verify_mulenpay_signature(request, raw_body): if not self._verify_mulenpay_signature(request, raw_body):
logger.warning( logger.warning(
'webhook signature verification failed, but processing anyway for debugging', '%s webhook signature verification failed, but processing anyway for debugging',
mulenpay_name=mulenpay_name, mulenpay_name,
) )
# return web.json_response({"status": "error", "reason": "invalid_signature"}, status=401) # return web.json_response({"status": "error", "reason": "invalid_signature"}, status=401)
try: try:
payload = json.loads(raw_body.decode('utf-8')) payload = json.loads(raw_body.decode('utf-8'))
except json.JSONDecodeError as error: except json.JSONDecodeError as error:
logger.error('Ошибка парсинга webhook', mulenpay_name=mulenpay_name, error=error) logger.error(f'Ошибка парсинга {mulenpay_name} webhook: {error}')
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400) return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
payment_service = PaymentService(self.bot) payment_service = PaymentService(self.bot)
@@ -185,7 +196,10 @@ class WebhookServer:
return web.json_response({'status': 'ok'}, status=200) return web.json_response({'status': 'ok'}, status=200)
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400) return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
except Exception as error: except Exception as error:
logger.error('Ошибка обработки webhook', mulenpay_name=mulenpay_name, error=error, exc_info=True) logger.error(
f'Ошибка обработки {mulenpay_name} webhook: {error}',
exc_info=True,
)
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500) return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
finally: finally:
try: try:
@@ -195,7 +209,10 @@ class WebhookServer:
except Exception as error: except Exception as error:
mulenpay_name = settings.get_mulenpay_display_name() mulenpay_name = settings.get_mulenpay_display_name()
logger.error('Критическая ошибка webhook', mulenpay_name=mulenpay_name, error=error, exc_info=True) logger.error(
f'Критическая ошибка {mulenpay_name} webhook: {error}',
exc_info=True,
)
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(error)}, status=500) return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(error)}, status=500)
@staticmethod @staticmethod
@@ -211,14 +228,14 @@ class WebhookServer:
secret_key = settings.MULENPAY_SECRET_KEY secret_key = settings.MULENPAY_SECRET_KEY
display_name = settings.get_mulenpay_display_name() display_name = settings.get_mulenpay_display_name()
if not secret_key: if not secret_key:
logger.error('secret key is not configured', display_name=display_name) logger.error('%s secret key is not configured', display_name)
return False return False
# Логируем все заголовки для отладки # Логируем все заголовки для отладки
logger.info('webhook headers for signature verification', display_name=display_name) logger.info('%s webhook headers for signature verification:', display_name)
for header_name, header_value in request.headers.items(): for header_name, header_value in request.headers.items():
if any(keyword in header_name.lower() for keyword in ['signature', 'sign', 'token', 'auth']): if any(keyword in header_name.lower() for keyword in ['signature', 'sign', 'token', 'auth']):
logger.info('log event', header_name=header_name, header_value=header_value) logger.info(f' {header_name}: {header_value}')
signature = WebhookServer._extract_mulenpay_header( signature = WebhookServer._extract_mulenpay_header(
request, request,
@@ -266,7 +283,7 @@ class WebhookServer:
if hmac.compare_digest(normalized_signature_no_padding, expected_urlsafe_base64_signature.rstrip('=')): if hmac.compare_digest(normalized_signature_no_padding, expected_urlsafe_base64_signature.rstrip('=')):
return True return True
logger.error('Неверная подпись webhook', display_name=display_name) logger.error('Неверная подпись %s webhook', display_name)
return False return False
authorization_header = request.headers.get('Authorization') authorization_header = request.headers.get('Authorization')
@@ -279,7 +296,7 @@ class WebhookServer:
if hmac.compare_digest(token, secret_key): if hmac.compare_digest(token, secret_key):
return True return True
logger.error('Неверный токен webhook', scheme=scheme, display_name=display_name) logger.error('Неверный %s токен %s webhook', scheme, display_name)
return False return False
if not value and hmac.compare_digest(token, secret_key): if not value and hmac.compare_digest(token, secret_key):
@@ -302,13 +319,13 @@ class WebhookServer:
{key: value for key, value in request.headers.items() if 'authorization' not in key.lower()}, {key: value for key, value in request.headers.items() if 'authorization' not in key.lower()},
) )
logger.error('Отсутствует подпись webhook', display_name=display_name) logger.error('Отсутствует подпись %s webhook', display_name)
return False return False
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response: async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
try: try:
logger.info('Получен Tribute webhook', method=request.method, path=request.path) logger.info(f'Получен Tribute webhook: {request.method} {request.path}')
logger.info('Headers', value=dict(request.headers)) logger.info(f'Headers: {dict(request.headers)}')
raw_body = await request.read() raw_body = await request.read()
@@ -317,17 +334,17 @@ class WebhookServer:
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400) return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
payload = raw_body.decode('utf-8') payload = raw_body.decode('utf-8')
logger.info('Payload', payload=payload) logger.info(f'Payload: {payload}')
try: try:
webhook_data = json.loads(payload) webhook_data = json.loads(payload)
logger.info('Распарсенные данные', webhook_data=webhook_data) logger.info(f'Распарсенные данные: {webhook_data}')
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error('Ошибка парсинга JSON', error=e) logger.error(f'Ошибка парсинга JSON: {e}')
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400) return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
signature = request.headers.get('trbt-signature') signature = request.headers.get('trbt-signature')
logger.info('Signature', signature=signature) logger.info(f'Signature: {signature}')
if not signature: if not signature:
logger.error('Отсутствует заголовок подписи Tribute webhook') logger.error('Отсутствует заголовок подписи Tribute webhook')
@@ -344,19 +361,19 @@ class WebhookServer:
result = await self.tribute_service.process_webhook(payload) result = await self.tribute_service.process_webhook(payload)
if result: if result:
logger.info('Tribute webhook обработан успешно', result=result) logger.info(f'Tribute webhook обработан успешно: {result}')
return web.json_response({'status': 'ok', 'result': result}, status=200) return web.json_response({'status': 'ok', 'result': result}, status=200)
logger.error('Ошибка обработки Tribute webhook') logger.error('Ошибка обработки Tribute webhook')
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400) return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
except Exception as e: except Exception as e:
logger.error('Критическая ошибка обработки Tribute webhook', error=e, exc_info=True) logger.error(f'Критическая ошибка обработки Tribute webhook: {e}', exc_info=True)
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(e)}, status=500) return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(e)}, status=500)
async def _cryptobot_webhook_handler(self, request: web.Request) -> web.Response: async def _cryptobot_webhook_handler(self, request: web.Request) -> web.Response:
try: try:
logger.info('Получен CryptoBot webhook', method=request.method, path=request.path) logger.info(f'Получен CryptoBot webhook: {request.method} {request.path}')
logger.info('Headers', value=dict(request.headers)) logger.info(f'Headers: {dict(request.headers)}')
raw_body = await request.read() raw_body = await request.read()
@@ -365,17 +382,17 @@ class WebhookServer:
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400) return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
payload = raw_body.decode('utf-8') payload = raw_body.decode('utf-8')
logger.info('CryptoBot Payload', payload=payload) logger.info(f'CryptoBot Payload: {payload}')
try: try:
webhook_data = json.loads(payload) webhook_data = json.loads(payload)
logger.info('CryptoBot данные', webhook_data=webhook_data) logger.info(f'CryptoBot данные: {webhook_data}')
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error('Ошибка парсинга CryptoBot JSON', error=e) logger.error(f'Ошибка парсинга CryptoBot JSON: {e}')
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400) return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
signature = request.headers.get('Crypto-Pay-API-Signature') signature = request.headers.get('Crypto-Pay-API-Signature')
logger.info('CryptoBot Signature', signature=signature) logger.info(f'CryptoBot Signature: {signature}')
if signature and settings.CRYPTOBOT_WEBHOOK_SECRET: if signature and settings.CRYPTOBOT_WEBHOOK_SECRET:
from app.external.cryptobot import CryptoBotService from app.external.cryptobot import CryptoBotService
@@ -400,7 +417,7 @@ class WebhookServer:
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400) return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
except Exception as e: except Exception as e:
logger.error('Критическая ошибка обработки CryptoBot webhook', error=e, exc_info=True) logger.error(f'Критическая ошибка обработки CryptoBot webhook: {e}', exc_info=True)
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(e)}, status=500) return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(e)}, status=500)
async def _health_check(self, request: web.Request) -> web.Response: async def _health_check(self, request: web.Request) -> web.Response:
@@ -431,22 +448,22 @@ class WebhookServer:
- CUR_ID: ID валюты/платежной системы - CUR_ID: ID валюты/платежной системы
""" """
try: try:
logger.info('Получен Freekassa webhook', method=request.method, path=request.path) logger.info(f'Получен Freekassa webhook: {request.method} {request.path}')
# Получаем IP клиента # Получаем IP клиента
client_ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip() client_ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip()
if not client_ip: if not client_ip:
client_ip = request.remote or 'unknown' client_ip = request.remote or 'unknown'
logger.info('Freekassa webhook IP', client_ip=client_ip) logger.info(f'Freekassa webhook IP: {client_ip}')
# Freekassa отправляет form-data # Freekassa отправляет form-data
try: try:
form_data = await request.post() form_data = await request.post()
except Exception as e: except Exception as e:
logger.error('Ошибка парсинга Freekassa form-data', error=e) logger.error(f'Ошибка парсинга Freekassa form-data: {e}')
return web.Response(text='NO', status=400) return web.Response(text='NO', status=400)
logger.info('Freekassa webhook data', value=dict(form_data)) logger.info(f'Freekassa webhook data: {dict(form_data)}')
# Извлекаем параметры # Извлекаем параметры
merchant_id = int(form_data.get('MERCHANT_ID', 0)) merchant_id = int(form_data.get('MERCHANT_ID', 0))
@@ -479,12 +496,12 @@ class WebhookServer:
) )
if success: if success:
logger.info('Freekassa webhook обработан успешно: order_id', order_id=order_id) logger.info(f'Freekassa webhook обработан успешно: order_id={order_id}')
# Freekassa ожидает YES в ответе # Freekassa ожидает YES в ответе
return web.Response(text='YES', status=200) return web.Response(text='YES', status=200)
logger.error('Ошибка обработки Freekassa webhook: order_id', order_id=order_id) logger.error(f'Ошибка обработки Freekassa webhook: order_id={order_id}')
return web.Response(text='NO', status=400) return web.Response(text='NO', status=400)
except Exception as e: except Exception as e:
logger.error('Критическая ошибка обработки Freekassa webhook', error=e, exc_info=True) logger.error(f'Критическая ошибка обработки Freekassa webhook: {e}', exc_info=True)
return web.Response(text='NO', status=500) return web.Response(text='NO', status=500)
+28 -33
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import logging
from collections.abc import Iterable from collections.abc import Iterable
from ipaddress import ( from ipaddress import (
IPv4Address, IPv4Address,
@@ -13,7 +14,6 @@ from ipaddress import (
) )
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Union
import structlog
from aiohttp import web from aiohttp import web
from app.config import settings from app.config import settings
@@ -23,7 +23,7 @@ from app.database.database import AsyncSessionLocal
if TYPE_CHECKING: if TYPE_CHECKING:
from app.services.payment_service import PaymentService from app.services.payment_service import PaymentService
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
IPAddress = Union[IPv4Address, IPv6Address] IPAddress = Union[IPv4Address, IPv6Address]
@@ -140,7 +140,7 @@ def _get_trusted_proxy_networks() -> tuple[IPNetwork, ...]:
try: try:
networks.append(ip_network(candidate, strict=False)) networks.append(ip_network(candidate, strict=False))
except ValueError: except ValueError:
logger.warning('Неверная сеть доверенного прокси YooKassa', candidate=candidate) logger.warning('Неверная сеть доверенного прокси YooKassa: %s', candidate)
cached_networks = tuple(networks) cached_networks = tuple(networks)
_TRUSTED_PROXY_NETWORKS_CACHE = (raw_value, cached_networks) _TRUSTED_PROXY_NETWORKS_CACHE = (raw_value, cached_networks)
@@ -206,8 +206,8 @@ class YooKassaWebhookHandler:
async def handle_webhook(self, request: web.Request) -> web.Response: async def handle_webhook(self, request: web.Request) -> web.Response:
try: try:
logger.info('📥 Получен YooKassa webhook', method=request.method, path=request.path) logger.info(f'📥 Получен YooKassa webhook: {request.method} {request.path}')
logger.info('📋 Headers', value=dict(request.headers)) logger.info(f'📋 Headers: {dict(request.headers)}')
header_ip_candidates = collect_yookassa_ip_candidates( header_ip_candidates = collect_yookassa_ip_candidates(
request.headers.get('X-Forwarded-For'), request.headers.get('X-Forwarded-For'),
@@ -221,8 +221,8 @@ class YooKassaWebhookHandler:
if client_ip is None: if client_ip is None:
logger.warning( logger.warning(
'🚫 Не удалось определить IP-адрес отправителя YooKassa webhook. Кандидаты', '🚫 Не удалось определить IP-адрес отправителя YooKassa webhook. Кандидаты: %s',
header_ip_candidates=header_ip_candidates + ([request.remote] if request.remote else []), header_ip_candidates + ([request.remote] if request.remote else []),
) )
return web.Response(status=403, text='Forbidden') return web.Response(status=403, text='Forbidden')
@@ -234,7 +234,7 @@ class YooKassaWebhookHandler:
) )
return web.Response(status=403, text='Forbidden') return web.Response(status=403, text='Forbidden')
logger.info('🌐 IP-адрес YooKassa подтверждён', client_ip=client_ip) logger.info('🌐 IP-адрес YooKassa подтверждён: %s', client_ip)
body = await request.text() body = await request.text()
@@ -242,20 +242,20 @@ class YooKassaWebhookHandler:
logger.warning('⚠️ Получен пустой webhook от YooKassa') logger.warning('⚠️ Получен пустой webhook от YooKassa')
return web.Response(status=400, text='Empty body') return web.Response(status=400, text='Empty body')
logger.info('📄 Body', body=body) logger.info(f'📄 Body: {body}')
signature = request.headers.get('Signature') or request.headers.get('X-YooKassa-Signature') signature = request.headers.get('Signature') or request.headers.get('X-YooKassa-Signature')
if signature: if signature:
logger.info('ℹ️ Получена подпись YooKassa', signature=signature) logger.info('ℹ️ Получена подпись YooKassa: %s', signature)
try: try:
webhook_data = json.loads(body) webhook_data = json.loads(body)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error('❌ Ошибка парсинга JSON webhook YooKassa', error=e) logger.error(f'❌ Ошибка парсинга JSON webhook YooKassa: {e}')
return web.Response(status=400, text='Invalid JSON') return web.Response(status=400, text='Invalid JSON')
logger.info('📊 Обработка webhook YooKassa', get=webhook_data.get('event', 'unknown_event')) logger.info(f'📊 Обработка webhook YooKassa: {webhook_data.get("event", "unknown_event")}')
logger.debug('🔍 Полные данные webhook', webhook_data=webhook_data) logger.debug(f'🔍 Полные данные webhook: {webhook_data}')
event_type = webhook_data.get('event') event_type = webhook_data.get('event')
if not event_type: if not event_type:
@@ -269,7 +269,7 @@ class YooKassaWebhookHandler:
return web.Response(status=400, text='No payment id') return web.Response(status=400, text='No payment id')
if event_type not in YOOKASSA_ALLOWED_EVENTS: if event_type not in YOOKASSA_ALLOWED_EVENTS:
logger.info('ℹ️ Игнорируем событие YooKassa', event_type=event_type) logger.info(f'ℹ️ Игнорируем событие YooKassa: {event_type}')
return web.Response(status=200, text='OK') return web.Response(status=200, text='OK')
async with AsyncSessionLocal() as db: async with AsyncSessionLocal() as db:
@@ -286,8 +286,7 @@ class YooKassaWebhookHandler:
if existing_transaction and event_type == 'payment.succeeded': if existing_transaction and event_type == 'payment.succeeded':
logger.info( logger.info(
'ℹ️ Платеж YooKassa уже был обработан. Пропускаем дублирующий вебхук.', f'️ Платеж YooKassa {yookassa_payment_id} уже был обработан. Пропускаем дублирующий вебхук.'
yookassa_payment_id=yookassa_payment_id,
) )
return web.Response(status=200, text='OK') return web.Response(status=200, text='OK')
@@ -296,26 +295,22 @@ class YooKassaWebhookHandler:
if success: if success:
await db.commit() await db.commit()
logger.info( logger.info(
'✅ Успешно обработан webhook YooKassa: для платежа', f'✅ Успешно обработан webhook YooKassa: {event_type} для платежа {yookassa_payment_id}'
event_type=event_type,
yookassa_payment_id=yookassa_payment_id,
) )
return web.Response(status=200, text='OK') return web.Response(status=200, text='OK')
await db.rollback() await db.rollback()
logger.error( logger.error(
'❌ Ошибка обработки webhook YooKassa: для платежа', f'❌ Ошибка обработки webhook YooKassa: {event_type} для платежа {yookassa_payment_id}'
event_type=event_type,
yookassa_payment_id=yookassa_payment_id,
) )
return web.Response(status=500, text='Processing error') return web.Response(status=500, text='Processing error')
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
logger.error('❌ Ошибка обработки webhook YooKassa', error=e, exc_info=True) logger.error(f'❌ Ошибка обработки webhook YooKassa: {e}', exc_info=True)
return web.Response(status=500, text='Processing error') return web.Response(status=500, text='Processing error')
except Exception as e: except Exception as e:
logger.error('❌ Критическая ошибка обработки webhook YooKassa', error=e, exc_info=True) logger.error(f'❌ Критическая ошибка обработки webhook YooKassa: {e}', exc_info=True)
return web.Response(status=500, text='Internal server error') return web.Response(status=500, text='Internal server error')
def setup_routes(self, app: web.Application) -> None: def setup_routes(self, app: web.Application) -> None:
@@ -324,7 +319,7 @@ class YooKassaWebhookHandler:
app.router.add_get(webhook_path, self._get_handler) app.router.add_get(webhook_path, self._get_handler)
app.router.add_options(webhook_path, self._options_handler) app.router.add_options(webhook_path, self._options_handler)
logger.info('✅ Настроен YooKassa webhook на пути: POST', webhook_path=webhook_path) logger.info(f'✅ Настроен YooKassa webhook на пути: POST {webhook_path}')
async def _get_handler(self, request: web.Request) -> web.Response: async def _get_handler(self, request: web.Request) -> web.Response:
return web.json_response( return web.json_response(
@@ -386,15 +381,15 @@ async def start_yookassa_webhook_server(payment_service: PaymentService) -> None
await site.start() await site.start()
logger.info( logger.info(
'✅ YooKassa webhook сервер запущен на', '✅ YooKassa webhook сервер запущен на %s:%s',
YOOKASSA_WEBHOOK_HOST=settings.YOOKASSA_WEBHOOK_HOST, settings.YOOKASSA_WEBHOOK_HOST,
YOOKASSA_WEBHOOK_PORT=settings.YOOKASSA_WEBHOOK_PORT, settings.YOOKASSA_WEBHOOK_PORT,
) )
logger.info( logger.info(
'🎯 YooKassa webhook URL: http://', '🎯 YooKassa webhook URL: http://%s:%s%s',
YOOKASSA_WEBHOOK_HOST=settings.YOOKASSA_WEBHOOK_HOST, settings.YOOKASSA_WEBHOOK_HOST,
YOOKASSA_WEBHOOK_PORT=settings.YOOKASSA_WEBHOOK_PORT, settings.YOOKASSA_WEBHOOK_PORT,
YOOKASSA_WEBHOOK_PATH=settings.YOOKASSA_WEBHOOK_PATH, settings.YOOKASSA_WEBHOOK_PATH,
) )
try: try:
@@ -408,5 +403,5 @@ async def start_yookassa_webhook_server(payment_service: PaymentService) -> None
logger.info('✅ YooKassa webhook сервер остановлен') logger.info('✅ YooKassa webhook сервер остановлен')
except Exception as e: except Exception as e:
logger.error('❌ Ошибка запуска YooKassa webhook сервера', error=e, exc_info=True) logger.error(f'❌ Ошибка запуска YooKassa webhook сервера: {e}', exc_info=True)
raise raise

Some files were not shown because too many files have changed in this diff Show More