Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e375252d65 | |||
| fa78fa6d09 | |||
| 11f8af003f | |||
| 11ef714e0d | |||
| 909a4039c4 | |||
| bf646112df | |||
| 8a6650e57c | |||
| 25e8c9f8fc | |||
| 1f0fef114b | |||
| be6036e879 | |||
| bba85a309a |
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.12.0"
|
||||
".": "3.12.1"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
|
||||
FROM python:3.13-slim
|
||||
|
||||
ARG VERSION="v3.12.0" # x-release-please-version
|
||||
ARG VERSION="v3.12.1" # x-release-please-version
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
|
||||
+16
-13
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
|
||||
import redis.asyncio as redis
|
||||
import structlog
|
||||
from aiogram import Bot, Dispatcher, types
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.fsm.storage.redis import RedisStorage
|
||||
@@ -63,6 +62,7 @@ from app.handlers.stars_payments import register_stars_handlers
|
||||
from app.middlewares.auth import AuthMiddleware
|
||||
from app.middlewares.blacklist import BlacklistMiddleware
|
||||
from app.middlewares.button_stats import ButtonStatsMiddleware
|
||||
from app.middlewares.context_binding import ContextVarsMiddleware
|
||||
from app.middlewares.global_error import GlobalErrorMiddleware
|
||||
from app.middlewares.logging import LoggingMiddleware
|
||||
from app.middlewares.maintenance import MaintenanceMiddleware
|
||||
@@ -75,14 +75,14 @@ from app.utils.message_patch import patch_message_methods
|
||||
|
||||
patch_message_methods()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def debug_callback_handler(callback: types.CallbackQuery):
|
||||
logger.info('🔍 DEBUG CALLBACK:')
|
||||
logger.info(f' - Data: {callback.data}')
|
||||
logger.info(f' - User: {callback.from_user.id}')
|
||||
logger.info(f' - Username: {callback.from_user.username}')
|
||||
logger.info('Data', callback_data=callback.data)
|
||||
logger.info('User', from_user_id=callback.from_user.id)
|
||||
logger.info('Username', username=callback.from_user.username)
|
||||
|
||||
|
||||
async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
@@ -90,7 +90,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
await cache.connect()
|
||||
logger.info('Кеш инициализирован')
|
||||
except Exception as e:
|
||||
logger.warning(f'Кеш не инициализирован: {e}')
|
||||
logger.warning('Кеш не инициализирован', error=e)
|
||||
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
@@ -106,12 +106,15 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
storage = RedisStorage(redis_client)
|
||||
logger.info('Подключено к Redis для FSM storage')
|
||||
except Exception as e:
|
||||
logger.warning(f'Не удалось подключиться к Redis: {e}')
|
||||
logger.warning('Не удалось подключиться к Redis', error=e)
|
||||
logger.info('Используется MemoryStorage для FSM')
|
||||
storage = MemoryStorage()
|
||||
|
||||
dp = Dispatcher(storage=storage)
|
||||
|
||||
dp.message.middleware(ContextVarsMiddleware())
|
||||
dp.callback_query.middleware(ContextVarsMiddleware())
|
||||
dp.pre_checkout_query.middleware(ContextVarsMiddleware())
|
||||
dp.message.middleware(GlobalErrorMiddleware())
|
||||
dp.callback_query.middleware(GlobalErrorMiddleware())
|
||||
dp.pre_checkout_query.middleware(GlobalErrorMiddleware())
|
||||
@@ -205,7 +208,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
await maintenance_service.start_monitoring()
|
||||
logger.info('Мониторинг техработ запущен')
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка запуска мониторинга техработ: {e}')
|
||||
logger.error('Ошибка запуска мониторинга техработ', error=e)
|
||||
else:
|
||||
logger.info('Мониторинг техработ отключен настройками')
|
||||
|
||||
@@ -236,7 +239,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
'Установите MINIAPP_CUSTOM_URL.'
|
||||
)
|
||||
elif settings.is_cabinet_mode():
|
||||
logger.info(f'🏠 Режим Cabinet активен, базовый URL: {settings.MINIAPP_CUSTOM_URL}')
|
||||
logger.info('🏠 Режим Cabinet активен, базовый URL', MINIAPP_CUSTOM_URL=settings.MINIAPP_CUSTOM_URL)
|
||||
|
||||
# Load per-section button styles cache
|
||||
if settings.is_cabinet_mode():
|
||||
@@ -245,7 +248,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
|
||||
await load_button_styles_cache()
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to load button styles cache: {e}')
|
||||
logger.warning('Failed to load button styles cache', error=e)
|
||||
|
||||
logger.info('Бот успешно настроен')
|
||||
|
||||
@@ -257,10 +260,10 @@ async def shutdown_bot():
|
||||
await maintenance_service.stop_monitoring()
|
||||
logger.info('Мониторинг техработ остановлен')
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка остановки мониторинга: {e}')
|
||||
logger.error('Ошибка остановки мониторинга', error=e)
|
||||
|
||||
try:
|
||||
await cache.close()
|
||||
logger.info('Соединения с кешем закрыты')
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка закрытия кеша: {e}')
|
||||
logger.error('Ошибка закрытия кеша', error=e)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"""OAuth 2.0 provider implementations for cabinet authentication."""
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.config import settings
|
||||
from app.utils.cache import cache, cache_key
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
STATE_TTL_SECONDS = 600 # 10 minutes
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""FastAPI dependencies for cabinet module."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from aiogram import Bot
|
||||
from fastapi import Depends, HTTPException, status
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
@@ -161,10 +161,12 @@ async def get_current_cabinet_user(
|
||||
except HTTPException:
|
||||
raise
|
||||
except TimeoutError:
|
||||
logger.warning(f'Timeout checking channel subscription for user {user.telegram_id}')
|
||||
logger.warning('Timeout checking channel subscription for user', telegram_id=user.telegram_id)
|
||||
# Don't block user if check times out
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to check channel subscription for user {user.telegram_id}: {e}')
|
||||
logger.warning(
|
||||
'Failed to check channel subscription for user', telegram_id=user.telegram_id, error=e
|
||||
)
|
||||
# Don't block user if check fails
|
||||
|
||||
return user
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Admin routes for managing VPN applications in app-config.json."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/apps', tags=['Cabinet Admin Apps'])
|
||||
|
||||
@@ -231,7 +231,7 @@ async def create_app(
|
||||
config['platforms'] = platforms
|
||||
|
||||
_save_config(config)
|
||||
logger.info(f"Admin {admin.id} created app '{request.app.id}' for platform '{platform}'")
|
||||
logger.info('Admin created app for platform', admin_id=admin.id, app_id=request.app.id, platform=platform)
|
||||
|
||||
return request.app
|
||||
|
||||
@@ -274,7 +274,7 @@ async def update_app(
|
||||
config['platforms'] = platforms
|
||||
|
||||
_save_config(config)
|
||||
logger.info(f"Admin {admin.id} updated app '{app_id}' in platform '{platform}'")
|
||||
logger.info('Admin updated app in platform', admin_id=admin.id, app_id=app_id, platform=platform)
|
||||
|
||||
return request.app
|
||||
|
||||
@@ -310,7 +310,7 @@ async def delete_app(
|
||||
config['platforms'] = platforms
|
||||
|
||||
_save_config(config)
|
||||
logger.info(f"Admin {admin.id} deleted app '{app_id}' from platform '{platform}'")
|
||||
logger.info('Admin deleted app from platform', admin_id=admin.id, app_id=app_id, platform=platform)
|
||||
|
||||
return {'status': 'deleted', 'app_id': app_id}
|
||||
|
||||
@@ -355,7 +355,7 @@ async def reorder_apps(
|
||||
config['platforms'] = platforms
|
||||
|
||||
_save_config(config)
|
||||
logger.info(f"Admin {admin.id} reordered apps in platform '{platform}'")
|
||||
logger.info('Admin reordered apps in platform', admin_id=admin.id, platform=platform)
|
||||
|
||||
return {'status': 'reordered', 'order': request.app_ids}
|
||||
|
||||
@@ -374,7 +374,7 @@ async def update_branding(
|
||||
config['config']['branding'] = request.branding.model_dump()
|
||||
|
||||
_save_config(config)
|
||||
logger.info(f'Admin {admin.id} updated branding')
|
||||
logger.info('Admin updated branding', admin_id=admin.id)
|
||||
|
||||
return request.branding
|
||||
|
||||
@@ -434,7 +434,14 @@ async def copy_app_to_platform(
|
||||
config['platforms'] = platforms
|
||||
|
||||
_save_config(config)
|
||||
logger.info(f"Admin {admin.id} copied app '{app_id}' from '{platform}' to '{target_platform}' as '{new_id}'")
|
||||
logger.info(
|
||||
'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}
|
||||
|
||||
@@ -498,9 +505,9 @@ async def set_remnawave_config_uuid(
|
||||
try:
|
||||
await bot_configuration_service.set_value(db, 'CABINET_REMNA_SUB_CONFIG', uuid_value)
|
||||
await db.commit()
|
||||
logger.info(f"Admin {admin.id} updated CABINET_REMNA_SUB_CONFIG to '{uuid_value}'")
|
||||
logger.info('Admin updated CABINET_REMNA_SUB_CONFIG to', admin_id=admin.id, uuid_value=uuid_value)
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving RemnaWave config UUID: {e}')
|
||||
logger.error('Error saving RemnaWave config UUID', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to save configuration',
|
||||
@@ -547,7 +554,7 @@ async def get_remnawave_subscription_config(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching RemnaWave config: {e}')
|
||||
logger.error('Error fetching RemnaWave config', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Failed to fetch config from RemnaWave: {e!s}',
|
||||
@@ -572,7 +579,7 @@ async def list_remnawave_subscription_configs(
|
||||
for c in configs
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f'Error listing RemnaWave configs: {e}')
|
||||
logger.error('Error listing RemnaWave configs', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Failed to fetch configs from RemnaWave: {e!s}',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Admin routes for Ban System monitoring in cabinet."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
||||
from app.config import settings
|
||||
@@ -45,7 +45,7 @@ from ..schemas.ban_system import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/ban-system', tags=['Cabinet Admin Ban System'])
|
||||
|
||||
@@ -53,9 +53,11 @@ router = APIRouter(prefix='/admin/ban-system', tags=['Cabinet Admin Ban System']
|
||||
def _get_ban_api() -> BanSystemAPI:
|
||||
"""Get Ban System API instance."""
|
||||
logger.debug(
|
||||
f'Ban System check - enabled: {settings.is_ban_system_enabled()}, configured: {settings.is_ban_system_configured()}'
|
||||
'Ban System check enabled: configured',
|
||||
is_ban_system_enabled=settings.is_ban_system_enabled(),
|
||||
is_ban_system_configured=settings.is_ban_system_configured(),
|
||||
)
|
||||
logger.debug(f'Ban System URL: {settings.get_ban_system_api_url()}')
|
||||
logger.debug('Ban System URL', get_ban_system_api_url=settings.get_ban_system_api_url())
|
||||
|
||||
if not settings.is_ban_system_enabled():
|
||||
raise HTTPException(
|
||||
@@ -83,13 +85,13 @@ async def _api_request(api: BanSystemAPI, method: str, *args, **kwargs) -> Any:
|
||||
func = getattr(api, method)
|
||||
return await func(*args, **kwargs)
|
||||
except BanSystemAPIError as e:
|
||||
logger.error(f'Ban System API error: {e}')
|
||||
logger.error('Ban System API error', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f'Ban System API error: {e.message}',
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Ban System unexpected error: {e}')
|
||||
logger.error('Ban System unexpected error', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Internal error: {e!s}',
|
||||
@@ -133,7 +135,7 @@ async def get_stats(
|
||||
api = _get_ban_api()
|
||||
data = await _api_request(api, 'get_stats')
|
||||
|
||||
logger.debug(f'Ban System raw stats: {data}')
|
||||
logger.debug('Ban System raw stats', data=data)
|
||||
|
||||
# Extract punishment stats
|
||||
punishment_stats = data.get('punishment_stats') or {}
|
||||
@@ -364,7 +366,7 @@ async def unban_user(
|
||||
api = _get_ban_api()
|
||||
try:
|
||||
await _api_request(api, 'enable_user', user_id=user_id)
|
||||
logger.info(f'Admin {admin.id} unbanned user {user_id} in Ban System')
|
||||
logger.info('Admin unbanned user in Ban System', admin_id=admin.id, user_id=user_id)
|
||||
return UnbanResponse(success=True, message='User unbanned successfully')
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -387,7 +389,7 @@ async def ban_user(
|
||||
minutes=request.minutes,
|
||||
reason=request.reason,
|
||||
)
|
||||
logger.info(f'Admin {admin.id} banned user {request.username}: {request.reason}')
|
||||
logger.info('Admin banned user', admin_id=admin.id, username=request.username, reason=request.reason)
|
||||
return UnbanResponse(success=True, message='User banned successfully')
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -819,7 +821,7 @@ async def set_setting(
|
||||
api = _get_ban_api()
|
||||
data = await _api_request(api, 'set_setting', key=key, value=value)
|
||||
|
||||
logger.info(f'Admin {admin.id} changed Ban System setting {key} to {value}')
|
||||
logger.info('Admin changed Ban System setting to', admin_id=admin.id, key=key, value=value)
|
||||
|
||||
return _parse_setting_response(key, data)
|
||||
|
||||
@@ -833,7 +835,7 @@ async def toggle_setting(
|
||||
api = _get_ban_api()
|
||||
data = await _api_request(api, 'toggle_setting', key=key)
|
||||
|
||||
logger.info(f'Admin {admin.id} toggled Ban System setting {key}')
|
||||
logger.info('Admin toggled Ban System setting', admin_id=admin.id, key=key)
|
||||
|
||||
return _parse_setting_response(key, data, default_type='bool')
|
||||
|
||||
@@ -850,7 +852,7 @@ async def whitelist_add(
|
||||
api = _get_ban_api()
|
||||
try:
|
||||
await _api_request(api, 'whitelist_add', username=request.username)
|
||||
logger.info(f'Admin {admin.id} added {request.username} to Ban System whitelist')
|
||||
logger.info('Admin added to Ban System whitelist', admin_id=admin.id, username=request.username)
|
||||
return UnbanResponse(success=True, message=f'User {request.username} added to whitelist')
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -867,7 +869,7 @@ async def whitelist_remove(
|
||||
api = _get_ban_api()
|
||||
try:
|
||||
await _api_request(api, 'whitelist_remove', username=request.username)
|
||||
logger.info(f'Admin {admin.id} removed {request.username} from Ban System whitelist')
|
||||
logger.info('Admin removed from Ban System whitelist', admin_id=admin.id, username=request.username)
|
||||
return UnbanResponse(success=True, message=f'User {request.username} removed from whitelist')
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Admin routes for broadcasts in cabinet."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import distinct, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -40,7 +40,7 @@ from ..schemas.broadcasts import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/broadcasts', tags=['Cabinet Admin Broadcasts'])
|
||||
|
||||
@@ -255,7 +255,7 @@ async def get_filters(
|
||||
try:
|
||||
count = await get_target_users_count(db, key)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to get count for filter {key}: {e}')
|
||||
logger.warning('Failed to get count for filter', key=key, error=e)
|
||||
count = 0
|
||||
filters.append(
|
||||
BroadcastFilter(
|
||||
@@ -272,7 +272,7 @@ async def get_filters(
|
||||
try:
|
||||
count = await get_target_users_count(db, key)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to get count for custom filter {key}: {e}')
|
||||
logger.warning('Failed to get count for custom filter', key=key, error=e)
|
||||
count = 0
|
||||
custom_filters.append(
|
||||
BroadcastFilter(
|
||||
@@ -367,7 +367,7 @@ async def preview_broadcast(
|
||||
try:
|
||||
count = await get_target_users_count(db, request.target)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get count for target {request.target}: {e}')
|
||||
logger.error('Failed to get count for target', target=request.target, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to count recipients',
|
||||
@@ -450,7 +450,9 @@ async def create_broadcast(
|
||||
await broadcast_service.start_broadcast(broadcast.id, config)
|
||||
await db.refresh(broadcast)
|
||||
|
||||
logger.info(f"Admin {admin.id} created broadcast {broadcast.id} for target '{request.target}'")
|
||||
logger.info(
|
||||
'Admin created broadcast for target', admin_id=admin.id, broadcast_id=broadcast.id, target=request.target
|
||||
)
|
||||
|
||||
return _serialize_broadcast(broadcast)
|
||||
|
||||
@@ -494,7 +496,7 @@ async def get_email_filters(
|
||||
try:
|
||||
count = await _get_email_filter_count(db, key)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to get count for email filter {key}: {e}')
|
||||
logger.warning('Failed to get count for email filter', key=key, error=e)
|
||||
count = 0
|
||||
|
||||
filters.append(
|
||||
@@ -532,7 +534,7 @@ async def preview_email_broadcast(
|
||||
try:
|
||||
count = await _get_email_filter_count(db, request.target)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get email count for target {request.target}: {e}')
|
||||
logger.error('Failed to get email count for target', target=request.target, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to count email recipients',
|
||||
@@ -661,7 +663,13 @@ async def create_combined_broadcast(
|
||||
|
||||
await db.refresh(broadcast)
|
||||
|
||||
logger.info(f"Admin {admin.id} created {request.channel} broadcast {broadcast.id} for target '{request.target}'")
|
||||
logger.info(
|
||||
'Admin created broadcast for target',
|
||||
admin_id=admin.id,
|
||||
channel=request.channel,
|
||||
broadcast_id=broadcast.id,
|
||||
target=request.target,
|
||||
)
|
||||
|
||||
return _serialize_broadcast(broadcast)
|
||||
|
||||
@@ -721,6 +729,6 @@ async def stop_broadcast(
|
||||
await db.commit()
|
||||
await db.refresh(broadcast)
|
||||
|
||||
logger.info(f'Admin {admin.id} stopped broadcast {broadcast_id}')
|
||||
logger.info('Admin stopped broadcast', admin_id=admin.id, broadcast_id=broadcast_id)
|
||||
|
||||
return _serialize_broadcast(broadcast)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Admin routes for per-section cabinet button style configuration."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/button-styles', tags=['Admin Button Styles'])
|
||||
|
||||
@@ -234,7 +234,9 @@ async def update_button_styles(
|
||||
# Refresh in-process cache
|
||||
await load_button_styles_cache()
|
||||
|
||||
logger.info('Admin %s updated button styles for sections: %s', admin.telegram_id, changed_sections)
|
||||
logger.info(
|
||||
'Admin updated button styles for sections', telegram_id=admin.telegram_id, changed_sections=changed_sections
|
||||
)
|
||||
|
||||
return _build_response(current)
|
||||
|
||||
@@ -248,6 +250,6 @@ async def reset_button_styles(
|
||||
await _set_setting_value(db, BUTTON_STYLES_KEY, json.dumps(DEFAULT_BUTTON_STYLES))
|
||||
await load_button_styles_cache()
|
||||
|
||||
logger.info('Admin %s reset button styles to defaults', admin.telegram_id)
|
||||
logger.info('Admin reset button styles to defaults', telegram_id=admin.telegram_id)
|
||||
|
||||
return _build_response(DEFAULT_BUTTON_STYLES)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Admin routes for managing advertising campaigns in cabinet."""
|
||||
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -45,7 +44,7 @@ from ..schemas.campaigns import (
|
||||
from ..schemas.tariffs import TariffListItem
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/campaigns', tags=['Cabinet Admin Campaigns'])
|
||||
|
||||
@@ -378,7 +377,7 @@ async def create_new_campaign(
|
||||
# Reload to get tariff relationship
|
||||
campaign = await get_campaign_by_id(db, campaign.id)
|
||||
|
||||
logger.info(f'Admin {admin.id} created campaign {campaign.id}: {campaign.name}')
|
||||
logger.info('Admin created campaign', admin_id=admin.id, campaign_id=campaign.id, campaign_name=campaign.name)
|
||||
|
||||
return await get_campaign(campaign.id, admin, db)
|
||||
|
||||
@@ -447,7 +446,7 @@ async def update_existing_campaign(
|
||||
if updates:
|
||||
await update_campaign(db, campaign, **updates)
|
||||
|
||||
logger.info(f'Admin {admin.id} updated campaign {campaign_id}')
|
||||
logger.info('Admin updated campaign', admin_id=admin.id, campaign_id=campaign_id)
|
||||
|
||||
return await get_campaign(campaign_id, admin, db)
|
||||
|
||||
@@ -475,7 +474,7 @@ async def delete_existing_campaign(
|
||||
)
|
||||
|
||||
await delete_campaign(db, campaign)
|
||||
logger.info(f'Admin {admin.id} deleted campaign {campaign_id}: {campaign.name}')
|
||||
logger.info('Admin deleted campaign', admin_id=admin.id, campaign_id=campaign_id, campaign_name=campaign.name)
|
||||
|
||||
return {'message': 'Campaign deleted successfully'}
|
||||
|
||||
@@ -498,7 +497,7 @@ async def toggle_campaign(
|
||||
await update_campaign(db, campaign, is_active=new_status)
|
||||
|
||||
status_text = 'activated' if new_status else 'deactivated'
|
||||
logger.info(f'Admin {admin.id} {status_text} campaign {campaign_id}')
|
||||
logger.info('Admin campaign', admin_id=admin.id, status_text=status_text, campaign_id=campaign_id)
|
||||
|
||||
return CampaignToggleResponse(
|
||||
id=campaign_id,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Admin routes for managing email notification templates."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -20,7 +20,7 @@ from ..services.email_template_overrides import (
|
||||
from ..services.email_templates import EmailNotificationTemplates
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/email-templates', tags=['Admin Email Templates'])
|
||||
|
||||
@@ -505,10 +505,7 @@ async def update_template(
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'Админ %s обновил email шаблон %s/%s',
|
||||
admin.id,
|
||||
notification_type,
|
||||
language,
|
||||
'Админ обновил email шаблон /', admin_id=admin.id, notification_type=notification_type, language=language
|
||||
)
|
||||
|
||||
return {'status': 'ok', 'template': result}
|
||||
@@ -533,10 +530,10 @@ async def reset_template(
|
||||
|
||||
if deleted:
|
||||
logger.info(
|
||||
'Админ %s сбросил email шаблон %s/%s к дефолту',
|
||||
admin.id,
|
||||
notification_type,
|
||||
language,
|
||||
'Админ сбросил email шаблон / к дефолту',
|
||||
admin_id=admin.id,
|
||||
notification_type=notification_type,
|
||||
language=language,
|
||||
)
|
||||
|
||||
return {'status': 'ok', 'was_custom': deleted}
|
||||
@@ -657,7 +654,7 @@ async def send_test_email(
|
||||
body_html=body_html,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error('Ошибка отправки тестового email: %s', e)
|
||||
logger.error('Ошибка отправки тестового email', e=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Failed to send test email: {e!s}',
|
||||
@@ -670,11 +667,11 @@ async def send_test_email(
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'Админ %s отправил тестовый email %s/%s на %s',
|
||||
admin.id,
|
||||
notification_type,
|
||||
language,
|
||||
to_email,
|
||||
'Админ отправил тестовый email / на',
|
||||
admin_id=admin.id,
|
||||
notification_type=notification_type,
|
||||
language=language,
|
||||
to_email=to_email,
|
||||
)
|
||||
|
||||
return {'status': 'ok', 'sent_to': to_email}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Admin routes for payment method configuration in cabinet."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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."""
|
||||
await update_sort_order(db, request.method_ids)
|
||||
logger.info(f'Admin {admin.id} updated payment methods order: {request.method_ids}')
|
||||
logger.info('Admin updated payment methods order', admin_id=admin.id, method_ids=request.method_ids)
|
||||
return {'success': True}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ async def update_payment_method(
|
||||
detail=f'Payment method not found: {method_id}',
|
||||
)
|
||||
|
||||
logger.info(f'Admin {admin.id} updated payment method config: {method_id}')
|
||||
logger.info('Admin updated payment method config', admin_id=admin.id, method_id=method_id)
|
||||
|
||||
defaults = _get_method_defaults()
|
||||
return _enrich_config(config, defaults)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Admin routes for payment verification in cabinet."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/payments', tags=['Cabinet Admin Payments'])
|
||||
|
||||
@@ -406,7 +406,14 @@ async def check_payment_status(
|
||||
if status_changed:
|
||||
_, new_status_text = _get_status_info(updated)
|
||||
message = f'Статус обновлён: {new_status_text}'
|
||||
logger.info(f'Admin {admin.id} checked payment {method}/{payment_id}: {old_status} -> {updated.status}')
|
||||
logger.info(
|
||||
'Admin checked payment /',
|
||||
admin_id=admin.id,
|
||||
method=method,
|
||||
payment_id=payment_id,
|
||||
old_status=old_status,
|
||||
status=updated.status,
|
||||
)
|
||||
else:
|
||||
message = 'Статус не изменился'
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Admin routes for pinned messages in cabinet."""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from aiogram import Bot
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
@@ -34,7 +34,7 @@ from ..schemas.pinned_messages import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/pinned-messages', tags=['Cabinet Admin Pinned Messages'])
|
||||
|
||||
@@ -186,7 +186,9 @@ async def create_pinned_message(
|
||||
if payload.broadcast:
|
||||
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
|
||||
|
||||
logger.info(f'Admin {admin.id} created pinned message #{msg.id} (broadcast={payload.broadcast})')
|
||||
logger.info(
|
||||
'Admin created pinned message # (broadcast=)', admin_id=admin.id, message_id=msg.id, broadcast=payload.broadcast
|
||||
)
|
||||
|
||||
return PinnedMessageBroadcastResponse(
|
||||
message=_serialize_pinned_message(msg),
|
||||
@@ -229,7 +231,7 @@ async def update_pinned_message(
|
||||
await db.commit()
|
||||
await db.refresh(msg)
|
||||
|
||||
logger.info(f'Admin {admin.id} updated pinned message #{message_id}')
|
||||
logger.info('Admin updated pinned message #', admin_id=admin.id, message_id=message_id)
|
||||
|
||||
return _serialize_pinned_message(msg)
|
||||
|
||||
@@ -273,7 +275,7 @@ async def deactivate_active_message(
|
||||
if not msg:
|
||||
return None
|
||||
|
||||
logger.info(f'Admin {admin.id} deactivated pinned message #{msg.id}')
|
||||
logger.info('Admin deactivated pinned message #', admin_id=admin.id, message_id=msg.id)
|
||||
|
||||
return _serialize_pinned_message(msg)
|
||||
|
||||
@@ -288,7 +290,12 @@ async def unpin_active_message(
|
||||
unpinned_count, failed_count, was_active = await unpin_active_pinned_message(_get_bot(), db)
|
||||
|
||||
if was_active:
|
||||
logger.info(f'Admin {admin.id} unpinned active message: unpinned={unpinned_count}, failed={failed_count}')
|
||||
logger.info(
|
||||
'Admin unpinned active message: unpinned=, failed',
|
||||
admin_id=admin.id,
|
||||
unpinned_count=unpinned_count,
|
||||
failed_count=failed_count,
|
||||
)
|
||||
|
||||
return PinnedMessageUnpinResponse(
|
||||
unpinned_count=unpinned_count,
|
||||
@@ -339,7 +346,9 @@ async def activate_pinned_message(
|
||||
if broadcast:
|
||||
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
|
||||
|
||||
logger.info(f'Admin {admin.id} activated pinned message #{message_id} (broadcast={broadcast})')
|
||||
logger.info(
|
||||
'Admin activated pinned message # (broadcast=)', admin_id=admin.id, message_id=message_id, broadcast=broadcast
|
||||
)
|
||||
|
||||
return PinnedMessageBroadcastResponse(
|
||||
message=_serialize_pinned_message(msg),
|
||||
@@ -364,7 +373,13 @@ async def broadcast_message(
|
||||
|
||||
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
|
||||
|
||||
logger.info(f'Admin {admin.id} broadcast pinned message #{message_id}: sent={sent_count}, failed={failed_count}')
|
||||
logger.info(
|
||||
'Admin broadcast pinned message #: sent=, failed',
|
||||
admin_id=admin.id,
|
||||
message_id=message_id,
|
||||
sent_count=sent_count,
|
||||
failed_count=failed_count,
|
||||
)
|
||||
|
||||
return PinnedMessageBroadcastResponse(
|
||||
message=_serialize_pinned_message(msg),
|
||||
@@ -394,4 +409,4 @@ async def delete_pinned_message(
|
||||
await db.delete(msg)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'Admin {admin.id} deleted pinned message #{message_id}')
|
||||
logger.info('Admin deleted pinned message #', admin_id=admin.id, message_id=message_id)
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from aiogram import Bot
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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:
|
||||
# Skip email-only users (no telegram_id)
|
||||
if not user.telegram_id:
|
||||
logger.debug(f'Skipping promo notification for email-only user {user.id}')
|
||||
logger.debug('Skipping promo notification for email-only user', user_id=user.id)
|
||||
return False
|
||||
|
||||
async with semaphore:
|
||||
@@ -459,18 +459,10 @@ async def _send_promo_notifications(
|
||||
)
|
||||
return True
|
||||
except (TelegramForbiddenError, TelegramBadRequest) as exc:
|
||||
logger.warning(
|
||||
'Failed to send promo notification to user %s: %s',
|
||||
user.telegram_id,
|
||||
exc,
|
||||
)
|
||||
logger.warning('Failed to send promo notification to user', telegram_id=user.telegram_id, exc=exc)
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
'Error sending promo notification to user %s: %s',
|
||||
user.telegram_id,
|
||||
exc,
|
||||
)
|
||||
logger.error('Error sending promo notification to user', telegram_id=user.telegram_id, exc=exc)
|
||||
return False
|
||||
|
||||
# Send in batches
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Admin routes for RemnaWave management in cabinet."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -76,7 +76,7 @@ except Exception:
|
||||
remnawave_sync_service = None
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/remnawave', tags=['Cabinet Admin RemnaWave'])
|
||||
|
||||
@@ -380,7 +380,9 @@ async def perform_node_action(
|
||||
}
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.telegram_id} performed {payload.action} on node {node_uuid}')
|
||||
logger.info(
|
||||
'Admin performed on node', telegram_id=admin.telegram_id, action=payload.action, node_uuid=node_uuid
|
||||
)
|
||||
return NodeActionResponse(
|
||||
success=True,
|
||||
message=messages.get(payload.action, 'Action completed'),
|
||||
@@ -403,7 +405,7 @@ async def restart_all_nodes(
|
||||
success = await service.restart_all_nodes()
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.telegram_id} restarted all nodes')
|
||||
logger.info('Admin restarted all nodes', telegram_id=admin.telegram_id)
|
||||
return NodeActionResponse(success=True, message='All nodes restart initiated')
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -510,7 +512,9 @@ async def create_squad(
|
||||
squad_uuid = await service.create_squad(payload.name, payload.inbound_uuids)
|
||||
|
||||
if squad_uuid:
|
||||
logger.info(f'Admin {admin.telegram_id} created squad {payload.name} ({squad_uuid})')
|
||||
logger.info(
|
||||
'Admin created squad', telegram_id=admin.telegram_id, payload_name=payload.name, squad_uuid=squad_uuid
|
||||
)
|
||||
return SquadOperationResponse(
|
||||
success=True,
|
||||
message='Squad created successfully',
|
||||
@@ -545,7 +549,7 @@ async def update_squad(
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.telegram_id} updated squad {squad_uuid}')
|
||||
logger.info('Admin updated squad', telegram_id=admin.telegram_id, squad_uuid=squad_uuid)
|
||||
return SquadOperationResponse(success=True, message='Squad updated')
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -594,7 +598,7 @@ async def perform_squad_action(
|
||||
message = 'Inbounds updated' if success else 'Failed to update inbounds'
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.telegram_id} performed {action} on squad {squad_uuid}')
|
||||
logger.info('Admin performed on squad', telegram_id=admin.telegram_id, action=action, squad_uuid=squad_uuid)
|
||||
|
||||
return SquadOperationResponse(success=success, message=message)
|
||||
|
||||
@@ -611,7 +615,7 @@ async def delete_squad(
|
||||
success = await service.delete_squad(squad_uuid)
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.telegram_id} deleted squad {squad_uuid}')
|
||||
logger.info('Admin deleted squad', telegram_id=admin.telegram_id, squad_uuid=squad_uuid)
|
||||
return SquadOperationResponse(success=True, message='Squad deleted')
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -699,7 +703,9 @@ async def migrate_squad_users(
|
||||
error=result.get('error'),
|
||||
)
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} migrated users from {source_uuid} to {target_uuid}')
|
||||
logger.info(
|
||||
'Admin migrated users from to', telegram_id=admin.telegram_id, source_uuid=source_uuid, target_uuid=target_uuid
|
||||
)
|
||||
|
||||
return MigrationResponse(
|
||||
success=True,
|
||||
@@ -782,14 +788,14 @@ async def toggle_auto_sync(
|
||||
if payload.enabled and not current_status.enabled:
|
||||
# Enable - would need to update settings and refresh schedule
|
||||
remnawave_sync_service.schedule_refresh(run_immediately=True)
|
||||
logger.info(f'Admin {admin.telegram_id} enabled auto sync')
|
||||
logger.info('Admin enabled auto sync', telegram_id=admin.telegram_id)
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
message='Auto sync enabled and scheduled',
|
||||
)
|
||||
if not payload.enabled and current_status.enabled:
|
||||
# Disable - would need to update settings and stop scheduler
|
||||
logger.info(f'Admin {admin.telegram_id} disabled auto sync')
|
||||
logger.info('Admin disabled auto sync', telegram_id=admin.telegram_id)
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
message='Auto sync setting change requested. Restart may be required.',
|
||||
@@ -811,7 +817,7 @@ async def run_auto_sync_now(
|
||||
detail='Auto sync service is not available',
|
||||
)
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} triggered manual sync')
|
||||
logger.info('Admin triggered manual sync', telegram_id=admin.telegram_id)
|
||||
result = await remnawave_sync_service.run_sync_now(reason='manual')
|
||||
|
||||
return AutoSyncRunResponse(
|
||||
@@ -839,7 +845,7 @@ async def sync_from_panel(
|
||||
|
||||
try:
|
||||
stats = await service.sync_users_from_panel(db, payload.mode)
|
||||
logger.info(f'Admin {admin.telegram_id} synced from panel (mode: {payload.mode})')
|
||||
logger.info('Admin synced from panel (mode: )', telegram_id=admin.telegram_id, mode=payload.mode)
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
message='Sync from panel completed',
|
||||
@@ -862,7 +868,7 @@ async def sync_to_panel(
|
||||
_ensure_configured(service)
|
||||
|
||||
stats = await service.sync_users_to_panel(db)
|
||||
logger.info(f'Admin {admin.telegram_id} synced to panel')
|
||||
logger.info('Admin synced to panel', telegram_id=admin.telegram_id)
|
||||
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
@@ -892,9 +898,15 @@ async def sync_servers(
|
||||
try:
|
||||
await cache.delete_pattern('available_countries*')
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to clear countries cache: {e}')
|
||||
logger.warning('Failed to clear countries cache', error=e)
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} synced servers: created={created}, updated={updated}, removed={removed}')
|
||||
logger.info(
|
||||
'Admin synced servers: created=, updated=, removed',
|
||||
telegram_id=admin.telegram_id,
|
||||
created=created,
|
||||
updated=updated,
|
||||
removed=removed,
|
||||
)
|
||||
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
@@ -918,7 +930,7 @@ async def validate_subscriptions(
|
||||
_ensure_configured(service)
|
||||
|
||||
stats = await service.validate_and_fix_subscriptions(db)
|
||||
logger.info(f'Admin {admin.telegram_id} validated subscriptions')
|
||||
logger.info('Admin validated subscriptions', telegram_id=admin.telegram_id)
|
||||
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
@@ -937,7 +949,7 @@ async def cleanup_subscriptions(
|
||||
_ensure_configured(service)
|
||||
|
||||
stats = await service.cleanup_orphaned_subscriptions(db)
|
||||
logger.info(f'Admin {admin.telegram_id} cleaned up subscriptions')
|
||||
logger.info('Admin cleaned up subscriptions', telegram_id=admin.telegram_id)
|
||||
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
@@ -956,7 +968,7 @@ async def sync_subscription_statuses(
|
||||
_ensure_configured(service)
|
||||
|
||||
stats = await service.sync_subscription_statuses(db)
|
||||
logger.info(f'Admin {admin.telegram_id} synced subscription statuses')
|
||||
logger.info('Admin synced subscription statuses', telegram_id=admin.telegram_id)
|
||||
|
||||
return SyncResponse(
|
||||
success=True,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Admin routes for managing servers in cabinet."""
|
||||
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import String, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -31,7 +30,7 @@ from ..schemas.servers import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/servers', tags=['Cabinet Admin Servers'])
|
||||
|
||||
@@ -184,7 +183,7 @@ async def update_existing_server(
|
||||
if request.promo_group_ids is not None:
|
||||
await update_server_squad_promo_groups(db, server_id, request.promo_group_ids)
|
||||
|
||||
logger.info(f'Admin {admin.id} updated server {server_id}')
|
||||
logger.info('Admin updated server', admin_id=admin.id, server_id=server_id)
|
||||
|
||||
return await get_server(server_id, admin, db)
|
||||
|
||||
@@ -207,7 +206,7 @@ async def toggle_server(
|
||||
await update_server_squad(db, server_id, is_available=new_status)
|
||||
|
||||
status_text = 'enabled' if new_status else 'disabled'
|
||||
logger.info(f'Admin {admin.id} {status_text} server {server_id}')
|
||||
logger.info('Admin server', admin_id=admin.id, status_text=status_text, server_id=server_id)
|
||||
|
||||
return ServerToggleResponse(
|
||||
id=server_id,
|
||||
@@ -234,7 +233,7 @@ async def toggle_server_trial(
|
||||
await update_server_squad(db, server_id, is_trial_eligible=new_status)
|
||||
|
||||
status_text = 'enabled for trial' if new_status else 'disabled for trial'
|
||||
logger.info(f'Admin {admin.id} {status_text} server {server_id}')
|
||||
logger.info('Admin server', admin_id=admin.id, status_text=status_text, server_id=server_id)
|
||||
|
||||
return ServerTrialToggleResponse(
|
||||
id=server_id,
|
||||
@@ -311,7 +310,7 @@ async def sync_servers(
|
||||
# Sync with database
|
||||
created, updated, removed = await sync_with_remnawave(db, squads)
|
||||
|
||||
logger.info(f'Admin {admin.id} synced servers: +{created} ~{updated} -{removed}')
|
||||
logger.info('Admin synced servers: + ~', admin_id=admin.id, created=created, updated=updated, removed=removed)
|
||||
|
||||
return ServerSyncResponse(
|
||||
created=created,
|
||||
@@ -323,7 +322,7 @@ async def sync_servers(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to sync servers: {e}')
|
||||
logger.error('Failed to sync servers', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Sync failed: {e!s}',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Admin settings routes for cabinet - system configuration management."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} updated setting {key} to {value}')
|
||||
logger.info('Admin updated setting to', telegram_id=admin.telegram_id, key=key, value=value)
|
||||
return _serialize_definition(definition)
|
||||
|
||||
|
||||
@@ -270,5 +270,5 @@ async def reset_setting(
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} reset setting {key}')
|
||||
logger.info('Admin reset setting', telegram_id=admin.telegram_id, key=key)
|
||||
return _serialize_definition(definition)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Admin routes for statistics dashboard in cabinet."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
_start_time = time.time()
|
||||
|
||||
@@ -317,7 +317,7 @@ async def get_dashboard_stats(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get dashboard stats: {e}')
|
||||
logger.error('Failed to get dashboard stats', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to load dashboard statistics',
|
||||
@@ -349,7 +349,7 @@ async def get_system_info(
|
||||
subscriptions_active=subscriptions_active,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get system info: {e}')
|
||||
logger.error('Failed to get system info', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to load system information',
|
||||
@@ -364,7 +364,7 @@ async def get_nodes_status(
|
||||
try:
|
||||
return await _get_nodes_overview()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get nodes status: {e}')
|
||||
logger.error('Failed to get nodes status', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to load nodes status',
|
||||
@@ -382,7 +382,7 @@ async def restart_node(
|
||||
success = await service.manage_node(node_uuid, 'restart')
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.id} restarted node {node_uuid}')
|
||||
logger.info('Admin restarted node', admin_id=admin.id, node_uuid=node_uuid)
|
||||
return {'success': True, 'message': 'Node restart initiated'}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -391,7 +391,7 @@ async def restart_node(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to restart node {node_uuid}: {e}')
|
||||
logger.error('Failed to restart node', node_uuid=node_uuid, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to restart node',
|
||||
@@ -420,7 +420,7 @@ async def toggle_node(
|
||||
success = await service.manage_node(node_uuid, action)
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.id} {action}d node {node_uuid}')
|
||||
logger.info('Admin d node', admin_id=admin.id, action=action, node_uuid=node_uuid)
|
||||
return {'success': True, 'message': f'Node {action}d', 'is_disabled': not is_disabled}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -429,7 +429,7 @@ async def toggle_node(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to toggle node {node_uuid}: {e}')
|
||||
logger.error('Failed to toggle node', node_uuid=node_uuid, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to toggle node',
|
||||
@@ -480,7 +480,7 @@ async def _get_nodes_overview() -> NodesOverview:
|
||||
nodes=node_statuses,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to get nodes from RemnaWave: {e}')
|
||||
logger.warning('Failed to get nodes from RemnaWave', error=e)
|
||||
# Return empty data if RemnaWave is unavailable
|
||||
return NodesOverview(
|
||||
total=0,
|
||||
@@ -560,7 +560,9 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
|
||||
)
|
||||
purchased_month = month_result.scalar() or 0
|
||||
|
||||
logger.info(f"📊 Тариф '{tariff.name}': активных={active_count}, триал={trial_count}")
|
||||
logger.info(
|
||||
'📊 Тариф активных=, триал', tariff_name=tariff.name, active_count=active_count, trial_count=trial_count
|
||||
)
|
||||
|
||||
tariff_items.append(
|
||||
TariffStatItem(
|
||||
@@ -576,7 +578,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
|
||||
|
||||
total_tariff_subscriptions += active_count
|
||||
|
||||
logger.info(f'📊 Всего подписок по тарифам: {total_tariff_subscriptions}')
|
||||
logger.info('📊 Всего подписок по тарифам', total_tariff_subscriptions=total_tariff_subscriptions)
|
||||
|
||||
return TariffStats(
|
||||
tariffs=tariff_items,
|
||||
@@ -584,7 +586,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get tariff stats: {e}', exc_info=True)
|
||||
logger.error('Failed to get tariff stats', error=e, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -800,7 +802,7 @@ async def get_top_referrers(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get top referrers: {e}', exc_info=True)
|
||||
logger.error('Failed to get top referrers', error=e, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to load referrers statistics',
|
||||
@@ -857,7 +859,7 @@ async def get_top_campaigns(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get top campaigns: {e}', exc_info=True)
|
||||
logger.error('Failed to get top campaigns', error=e, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to load campaigns statistics',
|
||||
@@ -996,7 +998,7 @@ async def get_recent_payments(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get recent payments: {e}', exc_info=True)
|
||||
logger.error('Failed to get recent payments', error=e, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to load recent payments',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Admin routes for managing tariffs in cabinet."""
|
||||
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -38,7 +37,7 @@ from ..schemas.tariffs import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/admin/tariffs', tags=['Cabinet Admin Tariffs'])
|
||||
|
||||
@@ -169,7 +168,7 @@ async def update_tariff_order(
|
||||
await reorder_tariffs(db, request.tariff_ids)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'Admin {admin.id} updated tariff order: {request.tariff_ids}')
|
||||
logger.info('Admin updated tariff order', admin_id=admin.id, tariff_ids=request.tariff_ids)
|
||||
|
||||
return {'message': 'Tariff order updated successfully'}
|
||||
|
||||
@@ -295,7 +294,7 @@ async def create_new_tariff(
|
||||
traffic_reset_mode=request.traffic_reset_mode,
|
||||
)
|
||||
|
||||
logger.info(f'Admin {admin.id} created tariff {tariff.id}: {tariff.name}')
|
||||
logger.info('Admin created tariff', admin_id=admin.id, tariff_id=tariff.id, tariff_name=tariff.name)
|
||||
|
||||
# Перезагружаем периоды из БД для синхронизации с ботом
|
||||
await load_period_prices_from_db(db)
|
||||
@@ -390,7 +389,7 @@ async def update_existing_tariff(
|
||||
if request.promo_group_ids is not None:
|
||||
await set_tariff_promo_groups(db, tariff, request.promo_group_ids)
|
||||
|
||||
logger.info(f'Admin {admin.id} updated tariff {tariff_id}')
|
||||
logger.info('Admin updated tariff', admin_id=admin.id, tariff_id=tariff_id)
|
||||
|
||||
# Перезагружаем периоды из БД для синхронизации с ботом
|
||||
await load_period_prices_from_db(db)
|
||||
@@ -414,7 +413,13 @@ async def delete_existing_tariff(
|
||||
|
||||
subs_count = await get_tariff_subscriptions_count(db, tariff_id)
|
||||
await delete_tariff(db, tariff)
|
||||
logger.info(f'Admin {admin.id} deleted tariff {tariff_id}: {tariff.name} (affected subscriptions: {subs_count})')
|
||||
logger.info(
|
||||
'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)
|
||||
@@ -440,7 +445,7 @@ async def toggle_tariff(
|
||||
await update_tariff(db, tariff, is_active=new_status)
|
||||
|
||||
status_text = 'activated' if new_status else 'deactivated'
|
||||
logger.info(f'Admin {admin.id} {status_text} tariff {tariff_id}')
|
||||
logger.info('Admin tariff', admin_id=admin.id, status_text=status_text, tariff_id=tariff_id)
|
||||
|
||||
# Перезагружаем периоды из БД для синхронизации с ботом
|
||||
await load_period_prices_from_db(db)
|
||||
@@ -483,7 +488,7 @@ async def toggle_trial_tariff(
|
||||
await update_tariff(db, tariff, is_trial_available=new_status)
|
||||
|
||||
status_text = 'set as trial' if new_status else 'removed from trial'
|
||||
logger.info(f'Admin {admin.id} {status_text} tariff {tariff_id}')
|
||||
logger.info('Admin tariff', admin_id=admin.id, status_text=status_text, tariff_id=tariff_id)
|
||||
|
||||
return TariffTrialResponse(
|
||||
id=tariff_id,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Admin tickets routes for cabinet."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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')
|
||||
logger.info('Updated ticket settings in .env file')
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to update .env file: {e}')
|
||||
logger.warning('Failed to update .env file', error=e)
|
||||
|
||||
return TicketSettingsResponse(
|
||||
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)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to notify user about ticket reply: {e}')
|
||||
logger.warning('Failed to notify user about ticket reply', error=e)
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to send Telegram notification: {e}')
|
||||
logger.warning('Failed to send Telegram notification', error=e)
|
||||
|
||||
# Уведомить пользователя в кабинете
|
||||
try:
|
||||
@@ -488,7 +488,7 @@ async def reply_to_ticket(
|
||||
# Отправить WebSocket уведомление
|
||||
await notify_user_ticket_reply(ticket.user_id, ticket.id, (request.message or '')[:100])
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to create cabinet notification for admin reply: {e}')
|
||||
logger.warning('Failed to create cabinet notification for admin reply', error=e)
|
||||
|
||||
return _message_to_response(message)
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import asyncio
|
||||
import csv
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from aiogram import Bot
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
@@ -32,7 +32,7 @@ from ..schemas.traffic import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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)
|
||||
return node.uuid, stats
|
||||
except Exception:
|
||||
logger.warning('Failed to get traffic for node %s', node.name, exc_info=True)
|
||||
logger.warning('Failed to get traffic for node', node_name=node.name, exc_info=True)
|
||||
return node.uuid, None
|
||||
|
||||
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)}',
|
||||
)
|
||||
except Exception:
|
||||
logger.error('Failed to send CSV to admin %s', admin.telegram_id, exc_info=True)
|
||||
logger.error('Failed to send CSV to admin', telegram_id=admin.telegram_id, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to send CSV report. Please try again later.',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Admin routes for version and release information."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import aiohttp
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.services.version_service import version_service
|
||||
from ..dependencies import get_current_admin_user
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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_last_check = datetime.now()
|
||||
logger.info('Fetched %d cabinet releases from GitHub', len(releases))
|
||||
logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases))
|
||||
return releases
|
||||
logger.warning('GitHub API returned status %d for cabinet releases', response.status)
|
||||
logger.warning('GitHub API returned status for cabinet releases', response_status=response.status)
|
||||
return _cabinet_cache.get('releases', [])
|
||||
except TimeoutError:
|
||||
logger.warning('Timeout fetching cabinet releases from GitHub')
|
||||
return _cabinet_cache.get('releases', [])
|
||||
except Exception as e:
|
||||
logger.error('Error fetching cabinet releases: %s', e)
|
||||
logger.error('Error fetching cabinet releases', e=e)
|
||||
return _cabinet_cache.get('releases', [])
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Admin routes for managing users in cabinet."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import Integer, and_, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -91,7 +91,7 @@ from ..schemas.users import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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()
|
||||
if not service.is_configured:
|
||||
logger.warning(f'Remnawave not configured, skipping panel sync for user {user.id}')
|
||||
logger.warning('Remnawave not configured, skipping panel sync for user', user_id=user.id)
|
||||
return {'skipped': True, 'reason': 'Remnawave not configured'}
|
||||
|
||||
is_active = (
|
||||
@@ -260,7 +260,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
|
||||
if panel_uuid:
|
||||
existing_user = await api.get_user_by_uuid(panel_uuid)
|
||||
if not existing_user:
|
||||
logger.warning(f'User {user.id} has stale remnawave_uuid {panel_uuid}, clearing')
|
||||
logger.warning('User has stale remnawave_uuid clearing', user_id=user.id, panel_uuid=panel_uuid)
|
||||
panel_uuid = None
|
||||
user.remnawave_uuid = None
|
||||
|
||||
@@ -299,7 +299,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
|
||||
try:
|
||||
await api.update_user(**update_kwargs)
|
||||
changes['action'] = 'updated'
|
||||
logger.info(f'Updated user {user.id} in Remnawave panel')
|
||||
logger.info('Updated user in Remnawave panel', user_id=user.id)
|
||||
except Exception as update_error:
|
||||
if hasattr(update_error, 'status_code') and update_error.status_code == 404:
|
||||
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
|
||||
changes['action'] = 'created'
|
||||
changes['panel_uuid'] = new_panel_user.uuid
|
||||
logger.info(f'Created user {user.id} in Remnawave panel: {new_panel_user.uuid}')
|
||||
logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid)
|
||||
|
||||
user.last_remnawave_sync = datetime.utcnow()
|
||||
await db.commit()
|
||||
@@ -336,7 +336,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
|
||||
return changes
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error syncing user {user.id} to panel: {e}')
|
||||
logger.error('Error syncing user to panel', user_id=user.id, error=e)
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
@@ -709,7 +709,7 @@ async def get_user_panel_info(
|
||||
last_node_name = node.node_name
|
||||
break
|
||||
except Exception:
|
||||
logger.warning(f'Failed to resolve node name for user {user_id}')
|
||||
logger.warning('Failed to resolve node name for user', user_id=user_id)
|
||||
|
||||
return UserPanelInfoResponse(
|
||||
found=True,
|
||||
@@ -728,7 +728,7 @@ async def get_user_panel_info(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting panel info for user {user_id}: {e}')
|
||||
logger.error('Error getting panel info for user', user_id=user_id, error=e)
|
||||
return UserPanelInfoResponse(found=False)
|
||||
|
||||
|
||||
@@ -812,7 +812,7 @@ async def get_user_node_usage(
|
||||
return UserNodeUsageResponse(items=items, categories=categories)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting node usage for user {user_id}: {e}')
|
||||
logger.error('Error getting node usage for user', user_id=user_id, error=e)
|
||||
return UserNodeUsageResponse(items=[])
|
||||
|
||||
|
||||
@@ -877,8 +877,12 @@ async def update_user_balance(
|
||||
await db.refresh(user)
|
||||
|
||||
logger.info(
|
||||
f'Admin {admin.id} updated balance for user {user_id}: '
|
||||
f'{old_balance} -> {user.balance_kopeks} ({request.amount_kopeks:+d})'
|
||||
'Admin updated balance for user',
|
||||
admin_id=admin.id,
|
||||
user_id=user_id,
|
||||
old_balance=old_balance,
|
||||
balance_kopeks=user.balance_kopeks,
|
||||
amount_kopeks=format(request.amount_kopeks, '+d'),
|
||||
)
|
||||
|
||||
return UpdateBalanceResponse(
|
||||
@@ -962,7 +966,7 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, new_sub)
|
||||
|
||||
logger.info(f'Admin {admin.id} created subscription for user {user_id}')
|
||||
logger.info('Admin created subscription for user', admin_id=admin.id, user_id=user_id)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -989,7 +993,9 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} extended subscription for user {user_id} by {request.days} days')
|
||||
logger.info(
|
||||
'Admin extended subscription for user by days', admin_id=admin.id, user_id=user_id, days=request.days
|
||||
)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1016,7 +1022,7 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} set end_date for user {user_id} subscription')
|
||||
logger.info('Admin set end_date for user subscription', admin_id=admin.id, user_id=user_id)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1050,7 +1056,7 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} changed tariff for user {user_id} to {tariff.name}')
|
||||
logger.info('Admin changed tariff for user to', admin_id=admin.id, user_id=user_id, tariff_name=tariff.name)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1071,7 +1077,7 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} updated traffic for user {user_id}')
|
||||
logger.info('Admin updated traffic for user', admin_id=admin.id, user_id=user_id)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1091,7 +1097,7 @@ async def update_user_subscription(
|
||||
await db.refresh(subscription)
|
||||
|
||||
state = 'enabled' if request.autopay_enabled else 'disabled'
|
||||
logger.info(f'Admin {admin.id} {state} autopay for user {user_id}')
|
||||
logger.info('Admin autopay for user', admin_id=admin.id, state=state, user_id=user_id)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1108,7 +1114,7 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} cancelled subscription for user {user_id}')
|
||||
logger.info('Admin cancelled subscription for user', admin_id=admin.id, user_id=user_id)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1127,7 +1133,7 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} activated subscription for user {user_id}')
|
||||
logger.info('Admin activated subscription for user', admin_id=admin.id, user_id=user_id)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1151,7 +1157,7 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} added {request.traffic_gb} GB traffic for user {user_id}')
|
||||
logger.info('Admin added traffic for user', admin_id=admin.id, traffic_gb=request.traffic_gb, user_id=user_id)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1211,7 +1217,11 @@ async def update_user_subscription(
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(
|
||||
f'Admin {admin.id} removed traffic purchase {request.traffic_purchase_id} ({removed_gb} GB) for user {user_id}'
|
||||
'Admin removed traffic purchase ( GB) for user',
|
||||
admin_id=admin.id,
|
||||
traffic_purchase_id=request.traffic_purchase_id,
|
||||
removed_gb=removed_gb,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
@@ -1234,7 +1244,9 @@ async def update_user_subscription(
|
||||
# Sync to Remnawave panel
|
||||
await _sync_subscription_to_panel(db, user, subscription)
|
||||
|
||||
logger.info(f'Admin {admin.id} set device limit to {request.device_limit} for user {user_id}')
|
||||
logger.info(
|
||||
'Admin set device limit to for user', admin_id=admin.id, device_limit=request.device_limit, user_id=user_id
|
||||
)
|
||||
|
||||
return UpdateSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1384,7 +1396,7 @@ async def update_user_status(
|
||||
if request.reason:
|
||||
action += f' (reason: {request.reason})'
|
||||
|
||||
logger.info(f'Admin {admin.id} changed status for user {user_id}: {action}')
|
||||
logger.info('Admin changed status for user', admin_id=admin.id, user_id=user_id, action=action)
|
||||
|
||||
return UpdateUserStatusResponse(
|
||||
success=True,
|
||||
@@ -1449,8 +1461,11 @@ async def update_user_restrictions(
|
||||
await db.refresh(user)
|
||||
|
||||
logger.info(
|
||||
f'Admin {admin.id} updated restrictions for user {user_id}: '
|
||||
f'topup={user.restriction_topup}, subscription={user.restriction_subscription}'
|
||||
'Admin updated restrictions for user topup=, subscription',
|
||||
admin_id=admin.id,
|
||||
user_id=user_id,
|
||||
restriction_topup=user.restriction_topup,
|
||||
restriction_subscription=user.restriction_subscription,
|
||||
)
|
||||
|
||||
return UpdateRestrictionsResponse(
|
||||
@@ -1501,7 +1516,11 @@ async def update_user_promo_group(
|
||||
await db.refresh(user)
|
||||
|
||||
logger.info(
|
||||
f'Admin {admin.id} changed promo group for user {user_id}: {old_promo_group_id} -> {new_promo_group_id}'
|
||||
'Admin changed promo group for user',
|
||||
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(
|
||||
@@ -1537,7 +1556,11 @@ async def update_user_referral_commission(
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
f'Admin {admin.id} changed referral commission for user {user_id}: {old_commission} -> {request.commission_percent}'
|
||||
'Admin changed referral commission for user',
|
||||
admin_id=admin.id,
|
||||
user_id=user_id,
|
||||
old_commission=old_commission,
|
||||
commission_percent=request.commission_percent,
|
||||
)
|
||||
|
||||
return UpdateReferralCommissionResponse(
|
||||
@@ -1600,7 +1623,7 @@ async def get_user_devices(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching devices for user {user_id}: {e}')
|
||||
logger.error('Error fetching devices for user', user_id=user_id, error=e)
|
||||
return UserDevicesResponse()
|
||||
|
||||
|
||||
@@ -1627,12 +1650,12 @@ async def delete_user_device(
|
||||
success = await api.remove_device(user.remnawave_uuid, hwid)
|
||||
|
||||
if success:
|
||||
logger.info(f'Admin {admin.id} deleted device {hwid} for user {user_id}')
|
||||
logger.info('Admin deleted device for user', admin_id=admin.id, hwid=hwid, user_id=user_id)
|
||||
return DeleteDeviceResponse(success=True, message='Device deleted', deleted_hwid=hwid)
|
||||
return DeleteDeviceResponse(success=False, message='Failed to delete device')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error deleting device {hwid} for user {user_id}: {e}')
|
||||
logger.error('Error deleting device for user', hwid=hwid, user_id=user_id, error=e)
|
||||
return DeleteDeviceResponse(success=False, message=str(e))
|
||||
|
||||
|
||||
@@ -1672,11 +1695,11 @@ async def reset_user_devices(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f'Admin {admin.id} reset devices for user {user_id}: {deleted}/{total}')
|
||||
logger.info('Admin reset devices for user /', admin_id=admin.id, user_id=user_id, deleted=deleted, total=total)
|
||||
return ResetDevicesResponse(success=True, message=f'Deleted {deleted}/{total} devices', deleted_count=deleted)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error resetting devices for user {user_id}: {e}')
|
||||
logger.error('Error resetting devices for user', user_id=user_id, error=e)
|
||||
return ResetDevicesResponse(success=False, message=str(e))
|
||||
|
||||
|
||||
@@ -1713,7 +1736,7 @@ async def delete_user(
|
||||
action = 'permanently deleted'
|
||||
|
||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||
logger.info(f'Admin {admin.id} {action} user {user_id}{reason_text}')
|
||||
logger.info('Admin user', admin_id=admin.id, action=action, user_id=user_id, reason_text=reason_text)
|
||||
|
||||
return DeleteUserResponse(
|
||||
success=True,
|
||||
@@ -1756,7 +1779,7 @@ async def full_delete_user(
|
||||
deleted_from_panel = request.delete_from_panel and user.remnawave_uuid is not None
|
||||
|
||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||
logger.info(f'Admin {admin.id} fully deleted user {user_id}{reason_text}')
|
||||
logger.info('Admin fully deleted user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
|
||||
|
||||
return FullDeleteUserResponse(
|
||||
success=success,
|
||||
@@ -1800,9 +1823,9 @@ async def reset_user_trial(
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
logger.info(f'Disabled Remnawave user {user.remnawave_uuid} for trial reset')
|
||||
logger.info('Disabled Remnawave user for trial reset', remnawave_uuid=user.remnawave_uuid)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to disable Remnawave user during trial reset: {e}')
|
||||
logger.warning('Failed to disable Remnawave user during trial reset', error=e)
|
||||
|
||||
# Delete subscription from database
|
||||
from sqlalchemy import delete
|
||||
@@ -1819,7 +1842,7 @@ async def reset_user_trial(
|
||||
await db.commit()
|
||||
|
||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||
logger.info(f'Admin {admin.id} reset trial for user {user_id}{reason_text}')
|
||||
logger.info('Admin reset trial for user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
|
||||
|
||||
return ResetTrialResponse(
|
||||
success=True,
|
||||
@@ -1871,10 +1894,10 @@ async def reset_user_subscription(
|
||||
subscription_service = SubscriptionService()
|
||||
panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
if panel_deactivated:
|
||||
logger.info(f'Disabled Remnawave user {user.remnawave_uuid} for subscription reset')
|
||||
logger.info('Disabled Remnawave user for subscription reset', remnawave_uuid=user.remnawave_uuid)
|
||||
except Exception as e:
|
||||
panel_error = str(e)
|
||||
logger.warning(f'Failed to disable Remnawave user during subscription reset: {e}')
|
||||
logger.warning('Failed to disable Remnawave user during subscription reset', error=e)
|
||||
|
||||
# Delete subscription from database
|
||||
from sqlalchemy import delete
|
||||
@@ -1888,7 +1911,7 @@ async def reset_user_subscription(
|
||||
await db.commit()
|
||||
|
||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||
logger.info(f'Admin {admin.id} reset subscription for user {user_id}{reason_text}')
|
||||
logger.info('Admin reset subscription for user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
|
||||
|
||||
return ResetSubscriptionResponse(
|
||||
success=True,
|
||||
@@ -1933,10 +1956,10 @@ async def disable_user(
|
||||
subscription_service = SubscriptionService()
|
||||
panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
if panel_deactivated:
|
||||
logger.info(f'Disabled Remnawave user {user.remnawave_uuid}')
|
||||
logger.info('Disabled Remnawave user', remnawave_uuid=user.remnawave_uuid)
|
||||
except Exception as e:
|
||||
panel_error = str(e)
|
||||
logger.warning(f'Failed to disable Remnawave user: {e}')
|
||||
logger.warning('Failed to disable Remnawave user', error=e)
|
||||
|
||||
# Deactivate subscription in bot database
|
||||
if user.subscription:
|
||||
@@ -1944,7 +1967,7 @@ async def disable_user(
|
||||
|
||||
await deactivate_subscription(db, user.subscription)
|
||||
subscription_deactivated = True
|
||||
logger.info(f'Deactivated subscription for user {user_id}')
|
||||
logger.info('Deactivated subscription for user', user_id=user_id)
|
||||
|
||||
# Block user account
|
||||
user.status = UserStatus.BLOCKED.value
|
||||
@@ -1952,7 +1975,7 @@ async def disable_user(
|
||||
await db.commit()
|
||||
|
||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||
logger.info(f'Admin {admin.id} disabled user {user_id}{reason_text}')
|
||||
logger.info('Admin disabled user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
|
||||
|
||||
return DisableUserResponse(
|
||||
success=True,
|
||||
@@ -2199,7 +2222,7 @@ async def get_user_sync_status(
|
||||
differences.append(f'Squads mismatch ({", ".join(squad_diff_parts)})')
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to get panel data for user {user_id}: {e}')
|
||||
logger.warning('Failed to get panel data for user', user_id=user_id, error=e)
|
||||
differences.append(f'Error fetching panel data: {e!s}')
|
||||
|
||||
return PanelSyncStatusResponse(
|
||||
@@ -2409,7 +2432,9 @@ async def sync_user_from_panel(
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'Admin {admin.id} synced user {user_id} from panel. Changes: {list(changes.keys())}')
|
||||
logger.info(
|
||||
'Admin synced user from panel. Changes', admin_id=admin.id, user_id=user_id, value=list(changes.keys())
|
||||
)
|
||||
|
||||
return SyncFromPanelResponse(
|
||||
success=True,
|
||||
@@ -2422,7 +2447,7 @@ async def sync_user_from_panel(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error syncing user {user_id} from panel: {e}')
|
||||
logger.error('Error syncing user from panel', user_id=user_id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Sync error: {e!s}',
|
||||
@@ -2510,7 +2535,7 @@ async def sync_user_to_panel(
|
||||
if panel_uuid:
|
||||
existing_user = await api.get_user_by_uuid(panel_uuid)
|
||||
if not existing_user:
|
||||
logger.warning(f'User {user.id} has stale remnawave_uuid {panel_uuid}, clearing')
|
||||
logger.warning('User has stale remnawave_uuid clearing', user_id=user.id, panel_uuid=panel_uuid)
|
||||
panel_uuid = None
|
||||
user.remnawave_uuid = None
|
||||
|
||||
@@ -2600,7 +2625,7 @@ async def sync_user_to_panel(
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'Admin {admin.id} synced user {user_id} to panel. Action: {action}')
|
||||
logger.info('Admin synced user to panel. Action', admin_id=admin.id, user_id=user_id, action=action)
|
||||
|
||||
return SyncToPanelResponse(
|
||||
success=True,
|
||||
@@ -2614,7 +2639,7 @@ async def sync_user_to_panel(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error syncing user {user_id} to panel: {e}')
|
||||
logger.error('Error syncing user to panel', user_id=user_id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Sync error: {e!s}',
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
API роуты колеса удачи для администраторов.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -35,7 +35,7 @@ from app.database.models import User
|
||||
from app.services.wheel_service import wheel_service
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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)
|
||||
|
||||
logger.info(f'🎡 Admin {admin.telegram_id} updated wheel config: {update_data}')
|
||||
logger.info('🎡 Admin updated wheel config', telegram_id=admin.telegram_id, update_data=update_data)
|
||||
|
||||
# Возвращаем полную конфигурацию
|
||||
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,
|
||||
)
|
||||
|
||||
logger.info(f'🎁 Admin {admin.telegram_id} created prize: {prize.display_name}')
|
||||
logger.info('🎁 Admin created prize', telegram_id=admin.telegram_id, display_name=prize.display_name)
|
||||
|
||||
return WheelPrizeAdminResponse(
|
||||
id=prize.id,
|
||||
@@ -261,7 +261,7 @@ async def update_prize(
|
||||
detail='Prize not found',
|
||||
)
|
||||
|
||||
logger.info(f'🎁 Admin {admin.telegram_id} updated prize {prize_id}: {update_data}')
|
||||
logger.info('🎁 Admin updated prize', telegram_id=admin.telegram_id, prize_id=prize_id, update_data=update_data)
|
||||
|
||||
return WheelPrizeAdminResponse(
|
||||
id=prize.id,
|
||||
@@ -298,7 +298,7 @@ async def delete_prize_endpoint(
|
||||
detail='Prize not found',
|
||||
)
|
||||
|
||||
logger.info(f'🗑️ Admin {admin.telegram_id} deleted prize {prize_id}')
|
||||
logger.info('🗑️ Admin deleted prize', telegram_id=admin.telegram_id, prize_id=prize_id)
|
||||
|
||||
|
||||
@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)
|
||||
logger.info(f'🔄 Admin {admin.telegram_id} reordered prizes: {request.prize_ids}')
|
||||
logger.info('🔄 Admin reordered prizes', telegram_id=admin.telegram_id, prize_ids=request.prize_ids)
|
||||
return {'success': True}
|
||||
|
||||
|
||||
|
||||
+45
-22
@@ -2,9 +2,9 @@
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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)
|
||||
|
||||
if not panel_users:
|
||||
logger.debug(f'No subscription found in panel for email: {user.email}')
|
||||
logger.debug('No subscription found in panel for email', email=user.email)
|
||||
return
|
||||
|
||||
# Take first user if multiple found
|
||||
panel_user = panel_users[0]
|
||||
logger.info(f'Found subscription in panel for email {user.email}: {panel_user.uuid}')
|
||||
logger.info('Found subscription in panel for email', email=user.email, uuid=panel_user.uuid)
|
||||
|
||||
# Link user to panel
|
||||
user.remnawave_uuid = panel_user.uuid
|
||||
@@ -208,7 +208,10 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
|
||||
existing_sub.device_limit = device_limit
|
||||
existing_sub.is_trial = False # Panel subscription is not trial
|
||||
logger.info(
|
||||
f'Updated subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}'
|
||||
'Updated subscription for email user squads: devices',
|
||||
email=user.email,
|
||||
connected_squads=connected_squads,
|
||||
device_limit=device_limit,
|
||||
)
|
||||
else:
|
||||
# Create new subscription (expire_at and current_time already naive UTC)
|
||||
@@ -228,13 +231,16 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
|
||||
)
|
||||
db.add(new_sub)
|
||||
logger.info(
|
||||
f'Created subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}'
|
||||
'Created subscription for email user squads: devices',
|
||||
email=user.email,
|
||||
connected_squads=connected_squads,
|
||||
device_limit=device_limit,
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to sync subscription from panel for {user.email}: {e}')
|
||||
logger.warning('Failed to sync subscription from panel for', email=user.email, error=e)
|
||||
# Don't rollback - it detaches user object and breaks subsequent operations
|
||||
# The sync is non-critical, main verification already succeeded
|
||||
|
||||
@@ -275,7 +281,7 @@ async def auth_telegram(
|
||||
|
||||
if not user:
|
||||
# Create new user from Telegram initData
|
||||
logger.info(f'Creating new user from cabinet (initData): telegram_id={telegram_id}')
|
||||
logger.info('Creating new user from cabinet (initData): telegram_id', telegram_id=telegram_id)
|
||||
user = await create_user(
|
||||
db=db,
|
||||
telegram_id=telegram_id,
|
||||
@@ -284,7 +290,7 @@ async def auth_telegram(
|
||||
last_name=tg_last_name,
|
||||
language=tg_language,
|
||||
)
|
||||
logger.info(f'User created successfully: id={user.id}, telegram_id={user.telegram_id}')
|
||||
logger.info('User created successfully: id=, telegram_id', user_id=user.id, telegram_id=user.telegram_id)
|
||||
else:
|
||||
# Update user info from initData (like bot middleware does)
|
||||
updated = False
|
||||
@@ -298,7 +304,7 @@ async def auth_telegram(
|
||||
user.last_name = tg_last_name
|
||||
updated = True
|
||||
if updated:
|
||||
logger.info(f'User {user.id} profile updated from initData')
|
||||
logger.info('User profile updated from initData', user_id=user.id)
|
||||
|
||||
if user.status != 'active':
|
||||
raise HTTPException(
|
||||
@@ -341,7 +347,9 @@ async def auth_telegram_widget(
|
||||
|
||||
if not user:
|
||||
# Create new user from Telegram data
|
||||
logger.info(f'Creating new user from cabinet: telegram_id={request.id}, username={request.username}')
|
||||
logger.info(
|
||||
'Creating new user from cabinet: telegram_id=, username', request_id=request.id, username=request.username
|
||||
)
|
||||
user = await create_user(
|
||||
db=db,
|
||||
telegram_id=request.id,
|
||||
@@ -350,7 +358,7 @@ async def auth_telegram_widget(
|
||||
last_name=request.last_name,
|
||||
language='ru',
|
||||
)
|
||||
logger.info(f'User created successfully: id={user.id}, telegram_id={user.telegram_id}')
|
||||
logger.info('User created successfully: id=, telegram_id', user_id=user.id, telegram_id=user.telegram_id)
|
||||
|
||||
if user.status != 'active':
|
||||
raise HTTPException(
|
||||
@@ -485,7 +493,7 @@ async def register_email_standalone(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Invalid test email password',
|
||||
)
|
||||
logger.info(f'Test email registration: {request.email}')
|
||||
logger.info('Test email registration', email=request.email)
|
||||
|
||||
# Check for disposable email
|
||||
if disposable_email_service.is_disposable(request.email):
|
||||
@@ -512,11 +520,17 @@ async def register_email_standalone(
|
||||
if referrer:
|
||||
# Защита от самореферала - нельзя регистрироваться по своему же коду
|
||||
if referrer.email and referrer.email.lower() == request.email.lower():
|
||||
logger.warning(f'Self-referral attempt blocked: email={request.email}, code={request.referral_code}')
|
||||
logger.warning(
|
||||
'Self-referral attempt blocked: email=, code',
|
||||
email=request.email,
|
||||
referral_code=request.referral_code,
|
||||
)
|
||||
referrer = None
|
||||
else:
|
||||
logger.info(
|
||||
f'Found referrer for email registration: referrer_id={referrer.id}, code={request.referral_code}'
|
||||
'Found referrer for email registration: referrer_id=, code',
|
||||
referrer_id=referrer.id,
|
||||
referral_code=request.referral_code,
|
||||
)
|
||||
|
||||
# Создать пользователя
|
||||
@@ -534,7 +548,7 @@ async def register_email_standalone(
|
||||
user.email_verified = True
|
||||
user.email_verified_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
logger.info(f'Test email auto-verified: {request.email}, user_id={user.id}')
|
||||
logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id)
|
||||
else:
|
||||
# Сгенерировать токен верификации
|
||||
verification_token = generate_verification_token()
|
||||
@@ -579,9 +593,11 @@ async def register_email_standalone(
|
||||
if referrer:
|
||||
try:
|
||||
await process_referral_registration(db, user.id, referrer.id, bot=None)
|
||||
logger.info(f'Processed referral registration: user_id={user.id}, referrer_id={referrer.id}')
|
||||
logger.info(
|
||||
'Processed referral registration: user_id=, referrer_id', user_id=user.id, referrer_id=referrer.id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to process referral registration: {e}')
|
||||
logger.error('Failed to process referral registration', error=e)
|
||||
# Не прерываем регистрацию из-за ошибки реферальной системы
|
||||
|
||||
# Для тестового email - сразу можно логиниться (уже verified)
|
||||
@@ -724,7 +740,7 @@ async def login_email(
|
||||
if not user:
|
||||
# For test email - auto-create user if not exists
|
||||
if is_test_email and settings.validate_test_email_password(request.email, request.password):
|
||||
logger.info(f'Test email login - creating new user: {request.email}')
|
||||
logger.info('Test email login creating new user', email=request.email)
|
||||
password_hash = hash_password(request.password)
|
||||
user = await create_user_by_email(
|
||||
db=db,
|
||||
@@ -1051,9 +1067,16 @@ async def request_email_change(
|
||||
custom_body_html=custom_body,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send verification email to {request.new_email} for user {user.id}: {e}')
|
||||
logger.error(
|
||||
'Failed to send verification email to for user',
|
||||
new_email=request.new_email,
|
||||
user_id=user.id,
|
||||
error=e,
|
||||
)
|
||||
|
||||
logger.info(f'Unverified email replaced for user {user.id}: {old_email} -> {request.new_email}')
|
||||
logger.info(
|
||||
'Unverified email replaced for user', user_id=user.id, old_email=old_email, new_email=request.new_email
|
||||
)
|
||||
|
||||
return EmailChangeResponse(
|
||||
message='Email replaced, verification sent to new address',
|
||||
@@ -1104,7 +1127,7 @@ async def request_email_change(
|
||||
detail='Email service is not configured',
|
||||
)
|
||||
|
||||
logger.info(f'Email change requested for user {user.id}: {user.email} -> {request.new_email}')
|
||||
logger.info('Email change requested for user', user_id=user.id, email=user.email, new_email=request.new_email)
|
||||
|
||||
return EmailChangeResponse(
|
||||
message='Verification code sent to new email',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Balance and payment routes for cabinet."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import desc, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -41,7 +41,7 @@ from ..schemas.balance import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/balance', tags=['Cabinet Balance'])
|
||||
|
||||
@@ -249,7 +249,7 @@ async def create_stars_invoice(
|
||||
if stars_amount <= 0:
|
||||
stars_amount = 1
|
||||
except Exception as e:
|
||||
logger.error(f'Error calculating Stars amount: {e}')
|
||||
logger.error('Error calculating Stars amount', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to calculate Stars amount',
|
||||
@@ -279,7 +279,7 @@ async def create_stars_invoice(
|
||||
result = response.json()
|
||||
|
||||
if not result.get('ok'):
|
||||
logger.error(f'Telegram API error: {result}')
|
||||
logger.error('Telegram API error', result=result)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create Stars invoice',
|
||||
@@ -287,8 +287,10 @@ async def create_stars_invoice(
|
||||
|
||||
invoice_url = result['result']
|
||||
logger.info(
|
||||
f'Created Stars invoice for balance top-up: user={user.id}, '
|
||||
f'amount={request.amount_kopeks} kopeks, stars={stars_amount}'
|
||||
'Created Stars invoice for balance top-up: user=, amount= kopeks, stars',
|
||||
user_id=user.id,
|
||||
amount_kopeks=request.amount_kopeks,
|
||||
stars_amount=stars_amount,
|
||||
)
|
||||
|
||||
return StarsInvoiceResponse(
|
||||
@@ -298,7 +300,7 @@ async def create_stars_invoice(
|
||||
)
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f'HTTP error creating Stars invoice: {e}')
|
||||
logger.error('HTTP error creating Stars invoice', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to connect to Telegram API',
|
||||
@@ -710,7 +712,7 @@ async def create_topup(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Payment creation error: {e}')
|
||||
logger.error('Payment creation error', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create payment. Please try again later.',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Branding routes for cabinet - logo, project name, and theme colors management."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/branding', tags=['Branding'])
|
||||
|
||||
@@ -307,7 +307,7 @@ async def update_branding_name(
|
||||
|
||||
await set_setting_value(db, BRANDING_NAME_KEY, name)
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} updated branding name to: {name}')
|
||||
logger.info('Admin updated branding name to', telegram_id=admin.telegram_id, name=name)
|
||||
|
||||
# Return updated branding
|
||||
custom_logo = has_custom_logo()
|
||||
@@ -368,7 +368,7 @@ async def upload_logo(
|
||||
# Mark that we have a custom logo
|
||||
await set_setting_value(db, BRANDING_LOGO_KEY, 'custom')
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} uploaded new logo: {logo_path}')
|
||||
logger.info('Admin uploaded new logo', telegram_id=admin.telegram_id, logo_path=logo_path)
|
||||
|
||||
# Get current name for response
|
||||
name = await get_setting_value(db, BRANDING_NAME_KEY)
|
||||
@@ -398,7 +398,7 @@ async def delete_logo(
|
||||
# Update setting
|
||||
await set_setting_value(db, BRANDING_LOGO_KEY, 'default')
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} deleted custom logo')
|
||||
logger.info('Admin deleted custom logo', telegram_id=admin.telegram_id)
|
||||
|
||||
# Get current name for response
|
||||
name = await get_setting_value(db, BRANDING_NAME_KEY)
|
||||
@@ -486,7 +486,7 @@ async def update_theme_colors(
|
||||
# Save to database
|
||||
await set_setting_value(db, THEME_COLORS_KEY, json.dumps(current_colors))
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} updated theme colors: {list(update_data.keys())}')
|
||||
logger.info('Admin updated theme colors', telegram_id=admin.telegram_id, value=list(update_data.keys()))
|
||||
|
||||
return ThemeColorsResponse(**current_colors)
|
||||
|
||||
@@ -500,7 +500,7 @@ async def reset_theme_colors(
|
||||
# Save default colors
|
||||
await set_setting_value(db, THEME_COLORS_KEY, json.dumps(DEFAULT_THEME_COLORS))
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} reset theme colors to defaults')
|
||||
logger.info('Admin reset theme colors to defaults', telegram_id=admin.telegram_id)
|
||||
|
||||
return ThemeColorsResponse(**DEFAULT_THEME_COLORS)
|
||||
|
||||
@@ -558,7 +558,7 @@ async def update_enabled_themes(
|
||||
# Save to database
|
||||
await set_setting_value(db, ENABLED_THEMES_KEY, json.dumps(current_themes))
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} updated enabled themes: {current_themes}')
|
||||
logger.info('Admin updated enabled themes', telegram_id=admin.telegram_id, current_themes=current_themes)
|
||||
|
||||
return EnabledThemesResponse(**current_themes)
|
||||
|
||||
@@ -593,7 +593,7 @@ async def update_animation_enabled(
|
||||
"""Update animation enabled setting. Admin only."""
|
||||
await set_setting_value(db, ANIMATION_ENABLED_KEY, str(payload.enabled).lower())
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} set animation enabled: {payload.enabled}')
|
||||
logger.info('Admin set animation enabled', telegram_id=admin.telegram_id, enabled=payload.enabled)
|
||||
|
||||
return AnimationEnabledResponse(enabled=payload.enabled)
|
||||
|
||||
@@ -628,7 +628,7 @@ async def update_fullscreen_enabled(
|
||||
"""Update fullscreen enabled setting. Admin only."""
|
||||
await set_setting_value(db, FULLSCREEN_ENABLED_KEY, str(payload.enabled).lower())
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} set fullscreen enabled: {payload.enabled}')
|
||||
logger.info('Admin set fullscreen enabled', telegram_id=admin.telegram_id, 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."""
|
||||
await set_setting_value(db, EMAIL_AUTH_ENABLED_KEY, str(payload.enabled).lower())
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} set email auth enabled: {payload.enabled}')
|
||||
logger.info('Admin set email auth enabled', telegram_id=admin.telegram_id, 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:
|
||||
await set_setting_value(db, GOOGLE_ADS_LABEL_KEY, payload.google_ads_label.strip())
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} updated analytics counters')
|
||||
logger.info('Admin updated analytics counters', telegram_id=admin.telegram_id)
|
||||
|
||||
# Return current state
|
||||
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."""
|
||||
await set_setting_value(db, LITE_MODE_ENABLED_KEY, str(payload.enabled).lower())
|
||||
|
||||
logger.info(f'Admin {admin.telegram_id} set lite mode enabled: {payload.enabled}')
|
||||
logger.info('Admin set lite mode enabled', telegram_id=admin.telegram_id, enabled=payload.enabled)
|
||||
|
||||
return LiteModeEnabledResponse(enabled=payload.enabled)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Contests routes for cabinet - user participation in games/contests."""
|
||||
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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.refresh(subscription)
|
||||
|
||||
logger.info(f'🎁 Extended subscription for user {user_id} by {days} days (contest prize)')
|
||||
logger.info('🎁 Extended subscription for user by days (contest prize)', user_id=user_id, days=days)
|
||||
return f'Subscription extended by {days} days'
|
||||
|
||||
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.refresh(user)
|
||||
|
||||
logger.info(f'🎁 Added {amount} to balance for user {user_id} (contest prize)')
|
||||
logger.info('🎁 Added to balance for user (contest prize)', amount=amount, user_id=user_id)
|
||||
return f'Balance increased by {amount}'
|
||||
|
||||
logger.warning(f'Unknown prize type: {prize_type}')
|
||||
logger.warning('Unknown prize type', prize_type=prize_type)
|
||||
return f"Prize type '{prize_type}' not supported"
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Info pages routes for cabinet - FAQ, rules, privacy policy, etc."""
|
||||
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -16,7 +15,7 @@ from app.services.public_offer_service import PublicOfferService
|
||||
from ..dependencies import get_cabinet_db, get_current_cabinet_user
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/info', tags=['Cabinet Info'])
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Media upload/download routes for cabinet tickets."""
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
|
||||
import structlog
|
||||
from aiogram import Bot
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
@@ -16,7 +16,7 @@ from app.database.models import User
|
||||
from ..dependencies import get_current_cabinet_user
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/media', tags=['Cabinet Media'])
|
||||
|
||||
@@ -125,7 +125,12 @@ async def upload_media(
|
||||
|
||||
media_url = _build_media_url(request, media.file_id)
|
||||
|
||||
logger.info(f'User {user.telegram_id} uploaded {media_type_normalized}: {media.file_id}')
|
||||
logger.info(
|
||||
'User uploaded',
|
||||
telegram_id=user.telegram_id,
|
||||
media_type_normalized=media_type_normalized,
|
||||
file_id=media.file_id,
|
||||
)
|
||||
|
||||
return MediaUploadResponse(
|
||||
media_type=media_type_normalized,
|
||||
@@ -136,7 +141,7 @@ async def upload_media(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as error:
|
||||
logger.error(f'Failed to upload media for user {user.telegram_id}: {error}')
|
||||
logger.error('Failed to upload media for user', telegram_id=user.telegram_id, error=error)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to upload media',
|
||||
@@ -187,7 +192,7 @@ async def download_media(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as error:
|
||||
logger.error(f'Failed to download media {file_id}: {error}')
|
||||
logger.error('Failed to download media', file_id=file_id, error=error)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to download media',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Notification settings routes for cabinet."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/notifications', tags=['Cabinet Notifications'])
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""OAuth 2.0 authentication routes for cabinet."""
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -27,7 +27,7 @@ from ..schemas.auth import AuthResponse
|
||||
from .auth import _create_auth_response, _store_refresh_token
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth'])
|
||||
|
||||
@@ -120,7 +120,7 @@ async def oauth_callback(
|
||||
try:
|
||||
token_data = await oauth_provider.exchange_code(request.code)
|
||||
except Exception as exc:
|
||||
logger.error('OAuth code exchange failed for %s: %s', provider, exc)
|
||||
logger.error('OAuth code exchange failed for', provider=provider, exc=exc)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Failed to exchange authorization code',
|
||||
@@ -130,7 +130,7 @@ async def oauth_callback(
|
||||
try:
|
||||
user_info: OAuthUserInfo = await oauth_provider.get_user_info(token_data)
|
||||
except Exception as exc:
|
||||
logger.error('OAuth user info fetch failed for %s: %s', provider, exc)
|
||||
logger.error('OAuth user info fetch failed for', provider=provider, exc=exc)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Failed to fetch user information from provider',
|
||||
@@ -139,7 +139,7 @@ async def oauth_callback(
|
||||
# 5. Find user by provider ID
|
||||
user = await get_user_by_oauth_provider(db, provider, user_info.provider_id)
|
||||
if user:
|
||||
logger.info('OAuth login via %s for existing user %s', provider, user.id)
|
||||
logger.info('OAuth login via for existing user', provider=provider, user_id=user.id)
|
||||
return await _finalize_oauth_login(db, user, 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)
|
||||
if user:
|
||||
await set_user_oauth_provider_id(db, user, provider, user_info.provider_id)
|
||||
logger.info('OAuth login via %s linked to existing email user %s', provider, user.id)
|
||||
logger.info('OAuth login via linked to existing email user', provider=provider, user_id=user.id)
|
||||
return await _finalize_oauth_login(db, user, provider)
|
||||
|
||||
# 7. Create new user
|
||||
@@ -161,5 +161,5 @@ async def oauth_callback(
|
||||
last_name=user_info.last_name,
|
||||
username=user_info.username,
|
||||
)
|
||||
logger.info('OAuth new user created via %s with id=%s', provider, user.id)
|
||||
logger.info('OAuth new user created via with id', provider=provider, user_id=user.id)
|
||||
return await _finalize_oauth_login(db, user, provider)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Polls routes for cabinet - user participation in polls/surveys."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/polls', tags=['Cabinet Polls'])
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Promo offers routes for cabinet - personal discounts and offers."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/promo', tags=['Cabinet Promo'])
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Promo code routes for cabinet."""
|
||||
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -12,7 +11,7 @@ from app.services.promocode_service import PromoCodeService
|
||||
from ..dependencies import get_cabinet_db, get_current_cabinet_user
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/promocode', tags=['Cabinet Promocode'])
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Referral program routes for cabinet."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import desc, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -22,7 +22,7 @@ from ..schemas.referral import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/referral', tags=['Cabinet Referral'])
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -58,7 +58,7 @@ from ..schemas.subscription import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/subscription', tags=['Cabinet Subscription'])
|
||||
|
||||
@@ -490,9 +490,9 @@ async def renew_subscription(
|
||||
|
||||
try:
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(f'Cart saved for auto-renewal (cabinet) user {user.id}')
|
||||
logger.info('Cart saved for auto-renewal (cabinet) user', user_id=user.id)
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving cart for auto-renewal (cabinet): {e}')
|
||||
logger.error('Error saving cart for auto-renewal (cabinet)', error=e)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
@@ -550,7 +550,7 @@ async def renew_subscription(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send admin notification for subscription renewal: {e}')
|
||||
logger.error('Failed to send admin notification for subscription renewal', error=e)
|
||||
|
||||
response = {
|
||||
'message': 'Subscription renewed successfully',
|
||||
@@ -789,11 +789,13 @@ async def purchase_traffic(
|
||||
try:
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(
|
||||
f'Cart saved for traffic purchase (cabinet) user {user.id}: '
|
||||
f'+{request.gb} GB, discount {traffic_discount_percent}%'
|
||||
'Cart saved for traffic purchase (cabinet) user + discount',
|
||||
user_id=user.id,
|
||||
gb=request.gb,
|
||||
traffic_discount_percent=traffic_discount_percent,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving cart for traffic purchase (cabinet): {e}')
|
||||
logger.error('Error saving cart for traffic purchase (cabinet)', error=e)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
@@ -833,7 +835,11 @@ async def purchase_traffic(
|
||||
from datetime import timedelta
|
||||
|
||||
subscription.traffic_reset_at = datetime.utcnow() + timedelta(days=30)
|
||||
logger.info(f'Set traffic_reset_at for subscription {subscription.id}: {subscription.traffic_reset_at}')
|
||||
logger.info(
|
||||
'Set traffic_reset_at for subscription',
|
||||
subscription_id=subscription.id,
|
||||
traffic_reset_at=subscription.traffic_reset_at,
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -845,7 +851,7 @@ async def purchase_traffic(
|
||||
else:
|
||||
await subscription_service.create_remnawave_user(db, subscription)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to sync traffic with RemnaWave: {e}')
|
||||
logger.error('Failed to sync traffic with RemnaWave', error=e)
|
||||
|
||||
# Создаём транзакцию
|
||||
await create_transaction(
|
||||
@@ -882,7 +888,7 @@ async def purchase_traffic(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send admin notification for traffic purchase: {e}')
|
||||
logger.error('Failed to send admin notification for traffic purchase', error=e)
|
||||
|
||||
response = {
|
||||
'success': True,
|
||||
@@ -946,9 +952,13 @@ async def purchase_devices_legacy(
|
||||
'source': 'cabinet',
|
||||
}
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(f'Cart saved for device purchase (cabinet /devices) user {user.id}: +{request.devices} devices')
|
||||
logger.info(
|
||||
'Cart saved for device purchase (cabinet /devices) user + devices',
|
||||
user_id=user.id,
|
||||
devices=request.devices,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving cart for device purchase (cabinet /devices): {e}')
|
||||
logger.error('Error saving cart for device purchase (cabinet /devices)', error=e)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
@@ -1020,7 +1030,7 @@ async def purchase_devices_legacy(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send admin notification for device purchase: {e}')
|
||||
logger.error('Failed to send admin notification for device purchase', error=e)
|
||||
|
||||
response = {
|
||||
'message': 'Devices added successfully',
|
||||
@@ -1123,7 +1133,7 @@ async def get_trial_info(
|
||||
if tariff_trial_days:
|
||||
duration_days = tariff_trial_days
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting trial tariff for info: {e}')
|
||||
logger.error('Error getting trial tariff for info', error=e)
|
||||
|
||||
# Check if user already has an active subscription
|
||||
if user.subscription:
|
||||
@@ -1211,7 +1221,7 @@ async def activate_trial(
|
||||
detail=f'Insufficient balance. Need {price_kopeks / 100:.2f} RUB',
|
||||
)
|
||||
user.balance_kopeks -= price_kopeks
|
||||
logger.info(f'User {user.id} paid {price_kopeks} kopeks for trial activation')
|
||||
logger.info('User paid kopeks for trial activation', user_id=user.id, price_kopeks=price_kopeks)
|
||||
|
||||
# Get trial parameters from tariff if configured (same logic as bot handler)
|
||||
trial_duration = settings.TRIAL_DURATION_DAYS
|
||||
@@ -1243,9 +1253,14 @@ async def activate_trial(
|
||||
tariff_trial_days = getattr(trial_tariff, 'trial_duration_days', None)
|
||||
if tariff_trial_days:
|
||||
trial_duration = tariff_trial_days
|
||||
logger.info(f'Using trial tariff {trial_tariff.name} (ID: {trial_tariff.id}) with squads: {trial_squads}')
|
||||
logger.info(
|
||||
'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:
|
||||
logger.error(f'Error getting trial tariff: {e}')
|
||||
logger.error('Error getting trial tariff', error=e)
|
||||
|
||||
# Create trial subscription
|
||||
subscription = await create_trial_subscription(
|
||||
@@ -1258,7 +1273,7 @@ async def activate_trial(
|
||||
tariff_id=tariff_id_for_trial,
|
||||
)
|
||||
|
||||
logger.info(f'Trial subscription activated for user {user.id}')
|
||||
logger.info('Trial subscription activated for user', user_id=user.id)
|
||||
|
||||
# Create RemnaWave user
|
||||
try:
|
||||
@@ -1267,7 +1282,7 @@ async def activate_trial(
|
||||
await subscription_service.create_remnawave_user(db, subscription)
|
||||
await db.refresh(subscription)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to create RemnaWave user for trial: {e}')
|
||||
logger.error('Failed to create RemnaWave user for trial', error=e)
|
||||
|
||||
# Send admin notification about trial activation
|
||||
try:
|
||||
@@ -1286,7 +1301,7 @@ async def activate_trial(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send trial activation notification: {e}')
|
||||
logger.error('Failed to send trial activation notification', error=e)
|
||||
|
||||
return _subscription_to_response(subscription)
|
||||
|
||||
@@ -1551,7 +1566,7 @@ async def get_purchase_options(
|
||||
detail=str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to build purchase options for user {user.id}: {e}')
|
||||
logger.error('Failed to build purchase options for user', user_id=user.id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to load purchase options',
|
||||
@@ -1596,7 +1611,7 @@ async def preview_purchase(
|
||||
detail=str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to calculate purchase preview for user {user.id}: {e}')
|
||||
logger.error('Failed to calculate purchase preview for user', user_id=user.id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to calculate price',
|
||||
@@ -1658,7 +1673,7 @@ async def submit_purchase(
|
||||
bot=None,
|
||||
)
|
||||
except Exception as notif_error:
|
||||
logger.warning(f'Failed to send subscription notification to {user.email}: {notif_error}')
|
||||
logger.warning('Failed to send subscription notification to', email=user.email, notif_error=notif_error)
|
||||
|
||||
# Отправляем уведомление админам о покупке подписки
|
||||
try:
|
||||
@@ -1684,7 +1699,7 @@ async def submit_purchase(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send admin notification for subscription purchase: {e}')
|
||||
logger.error('Failed to send admin notification for subscription purchase', error=e)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
@@ -1716,9 +1731,9 @@ async def submit_purchase(
|
||||
'source': 'cabinet',
|
||||
}
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(f'Cart saved for auto-purchase (cabinet /purchase) user {user.id}')
|
||||
logger.info('Cart saved for auto-purchase (cabinet /purchase) user', user_id=user.id)
|
||||
except Exception as cart_error:
|
||||
logger.error(f'Error saving cart for auto-purchase (cabinet /purchase): {cart_error}')
|
||||
logger.error('Error saving cart for auto-purchase (cabinet /purchase)', cart_error=cart_error)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
@@ -1730,7 +1745,7 @@ async def submit_purchase(
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to submit purchase for user {user.id}: {e}')
|
||||
logger.error('Failed to submit purchase for user', user_id=user.id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to process purchase',
|
||||
@@ -1912,9 +1927,9 @@ async def purchase_tariff(
|
||||
|
||||
try:
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(f'Cart saved for auto-purchase (cabinet) user {user.id}, tariff {tariff.id}')
|
||||
logger.info('Cart saved for auto-purchase (cabinet) user tariff', user_id=user.id, tariff_id=tariff.id)
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving cart for auto-purchase (cabinet): {e}')
|
||||
logger.error('Error saving cart for auto-purchase (cabinet)', error=e)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
@@ -2019,7 +2034,7 @@ async def purchase_tariff(
|
||||
reset_reason='покупка тарифа (cabinet)',
|
||||
)
|
||||
except Exception as remnawave_error:
|
||||
logger.error(f'Failed to sync subscription with RemnaWave: {remnawave_error}')
|
||||
logger.error('Failed to sync subscription with RemnaWave', remnawave_error=remnawave_error)
|
||||
|
||||
# Save cart for auto-renewal (not for daily tariffs - they have their own charging)
|
||||
if not is_daily_tariff:
|
||||
@@ -2033,9 +2048,9 @@ async def purchase_tariff(
|
||||
'description': f'Продление тарифа {tariff.name} на {period_days} дней',
|
||||
}
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(f'Tariff cart saved for auto-renewal (cabinet) user {user.id}')
|
||||
logger.info('Tariff cart saved for auto-renewal (cabinet) user', user_id=user.id)
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving tariff cart (cabinet): {e}')
|
||||
logger.error('Error saving tariff cart (cabinet)', error=e)
|
||||
|
||||
await db.refresh(user)
|
||||
|
||||
@@ -2095,7 +2110,7 @@ async def purchase_tariff(
|
||||
bot=None,
|
||||
)
|
||||
except Exception as notif_error:
|
||||
logger.warning(f'Failed to send subscription notification to {user.email}: {notif_error}')
|
||||
logger.warning('Failed to send subscription notification to', email=user.email, notif_error=notif_error)
|
||||
|
||||
# Отправляем уведомление админам о покупке/продлении тарифа
|
||||
try:
|
||||
@@ -2124,14 +2139,14 @@ async def purchase_tariff(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send admin notification for tariff purchase: {e}')
|
||||
logger.error('Failed to send admin notification for tariff purchase', error=e)
|
||||
|
||||
return response
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to purchase tariff for user {user.id}: {e}')
|
||||
logger.error('Failed to purchase tariff for user', user_id=user.id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to process tariff purchase',
|
||||
@@ -2238,11 +2253,13 @@ async def purchase_devices(
|
||||
}
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(
|
||||
f'Cart saved for device purchase (cabinet) user {user.id}: '
|
||||
f'+{request.devices} devices, discount {devices_discount_percent}%'
|
||||
'Cart saved for device purchase (cabinet) user + devices, discount',
|
||||
user_id=user.id,
|
||||
devices=request.devices,
|
||||
devices_discount_percent=devices_discount_percent,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving cart for device purchase (cabinet): {e}')
|
||||
logger.error('Error saving cart for device purchase (cabinet)', error=e)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
@@ -2288,17 +2305,23 @@ async def purchase_devices(
|
||||
else:
|
||||
await service.create_remnawave_user(db, subscription)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to sync devices with RemnaWave: {e}')
|
||||
logger.error('Failed to sync devices with RemnaWave', error=e)
|
||||
|
||||
await db.refresh(user)
|
||||
|
||||
if devices_discount_percent > 0:
|
||||
logger.info(
|
||||
f'User {user.id} purchased {request.devices} devices for {price_kopeks} kopeks '
|
||||
f'(discount {devices_discount_percent}%, saved {discount_value} kopeks)'
|
||||
'User purchased devices for kopeks (discount saved kopeks)',
|
||||
user_id=user.id,
|
||||
devices=request.devices,
|
||||
price_kopeks=price_kopeks,
|
||||
devices_discount_percent=devices_discount_percent,
|
||||
discount_value=discount_value,
|
||||
)
|
||||
else:
|
||||
logger.info(f'User {user.id} purchased {request.devices} devices for {price_kopeks} kopeks')
|
||||
logger.info(
|
||||
'User purchased devices for kopeks', user_id=user.id, devices=request.devices, price_kopeks=price_kopeks
|
||||
)
|
||||
|
||||
# Отправляем уведомление админам
|
||||
try:
|
||||
@@ -2322,7 +2345,7 @@ async def purchase_devices(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send admin notification for device purchase: {e}')
|
||||
logger.error('Failed to send admin notification for device purchase', error=e)
|
||||
|
||||
response = {
|
||||
'success': True,
|
||||
@@ -2345,7 +2368,7 @@ async def purchase_devices(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to purchase devices for user {user.id}: {e}')
|
||||
logger.error('Failed to purchase devices for user', user_id=user.id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Не удалось обработать покупку устройств',
|
||||
@@ -2463,7 +2486,7 @@ async def save_traffic_cart(
|
||||
'description': f'Докупка {request.gb} ГБ трафика',
|
||||
}
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(f'Cart saved for traffic purchase (cabinet save-cart) user {user.id}: +{request.gb} GB')
|
||||
logger.info('Cart saved for traffic purchase (cabinet save-cart) user +', user_id=user.id, gb=request.gb)
|
||||
|
||||
return {'success': True, 'cart_saved': True}
|
||||
|
||||
@@ -2538,7 +2561,9 @@ async def save_devices_cart(
|
||||
'source': 'cabinet',
|
||||
}
|
||||
await user_cart_service.save_user_cart(user.id, cart_data)
|
||||
logger.info(f'Cart saved for device purchase (cabinet save-cart) user {user.id}: +{request.devices} devices')
|
||||
logger.info(
|
||||
'Cart saved for device purchase (cabinet save-cart) user + devices', user_id=user.id, devices=request.devices
|
||||
)
|
||||
|
||||
return {'success': True, 'cart_saved': True}
|
||||
|
||||
@@ -2664,7 +2689,7 @@ def _load_app_config_from_file() -> dict[str, Any]:
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to load app-config.json: {e}')
|
||||
logger.error('Failed to load app-config.json', error=e)
|
||||
return {}
|
||||
|
||||
|
||||
@@ -2807,10 +2832,11 @@ def _get_url_scheme_for_app(app: dict[str, Any]) -> tuple[str, bool]:
|
||||
|
||||
# No scheme found
|
||||
logger.debug(
|
||||
f"_get_url_scheme_for_app: No scheme found for app '{app.get('name')}', "
|
||||
f'has blocks: {bool(app.get("blocks"))}, '
|
||||
f'has buttons: {bool(app.get("buttons"))}, '
|
||||
f'has urlScheme: {bool(app.get("urlScheme"))}'
|
||||
'_get_url_scheme_for_app: No scheme found for app has blocks: has buttons: has urlScheme',
|
||||
get=app.get('name'),
|
||||
get_2=bool(app.get('blocks')),
|
||||
get_3=bool(app.get('buttons')),
|
||||
get_4=bool(app.get('urlScheme')),
|
||||
)
|
||||
return '', False
|
||||
|
||||
@@ -2856,7 +2882,7 @@ def _convert_remnawave_app_to_cabinet(app: dict[str, Any]) -> dict[str, Any]:
|
||||
# Debug log for conversion (не логируем отсутствие urlScheme - для Happ это нормально)
|
||||
app_name = app.get('name', 'unknown')
|
||||
if url_scheme:
|
||||
logger.debug(f"_convert_remnawave_app_to_cabinet: app '{app_name}' -> urlScheme='{url_scheme}'")
|
||||
logger.debug('_convert_remnawave_app_to_cabinet: app urlScheme', app_name=app_name, url_scheme=url_scheme)
|
||||
|
||||
# Smart block mapping: find blocks by their content, not just position
|
||||
# 1. First block is usually installation
|
||||
@@ -2965,12 +2991,12 @@ async def _load_app_config_async() -> dict[str, Any]:
|
||||
async with service.get_api_client() as api:
|
||||
config = await api.get_subscription_page_config(remnawave_uuid)
|
||||
if config and config.config:
|
||||
logger.debug(f'Loaded app config from RemnaWave: {remnawave_uuid}')
|
||||
logger.debug('Loaded app config from RemnaWave', remnawave_uuid=remnawave_uuid)
|
||||
raw = dict(config.config)
|
||||
raw['_isRemnawave'] = True
|
||||
return raw
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to load RemnaWave config, falling back to file: {e}')
|
||||
logger.warning('Failed to load RemnaWave config, falling back to file', error=e)
|
||||
|
||||
# Fallback to local file
|
||||
return _load_app_config_from_file()
|
||||
@@ -3001,21 +3027,21 @@ def _create_deep_link(
|
||||
|
||||
scheme, uses_crypto = _get_url_scheme_for_app(app)
|
||||
if not scheme:
|
||||
logger.debug(f"_create_deep_link: no urlScheme for app '{app.get('name', 'unknown')}'")
|
||||
logger.debug('_create_deep_link: no urlScheme for app', get=app.get('name', 'unknown'))
|
||||
return None
|
||||
|
||||
# Pick the correct payload based on which template the app uses
|
||||
if uses_crypto:
|
||||
if not subscription_crypto_link:
|
||||
logger.debug(
|
||||
f"_create_deep_link: app '{app.get('name', 'unknown')}' requires crypto link but none available"
|
||||
'_create_deep_link: app requires crypto link but none available', get=app.get('name', 'unknown')
|
||||
)
|
||||
return None
|
||||
payload = subscription_crypto_link
|
||||
else:
|
||||
if not subscription_url:
|
||||
logger.debug(
|
||||
f"_create_deep_link: app '{app.get('name', 'unknown')}' requires subscription_url but none available"
|
||||
'_create_deep_link: app requires subscription_url but none available', get=app.get('name', 'unknown')
|
||||
)
|
||||
return None
|
||||
payload = subscription_url
|
||||
@@ -3024,7 +3050,7 @@ def _create_deep_link(
|
||||
try:
|
||||
payload = base64.b64encode(payload.encode('utf-8')).decode('utf-8')
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to encode payload to base64: {e}')
|
||||
logger.warning('Failed to encode payload to base64', error=e)
|
||||
|
||||
return f'{scheme}{payload}'
|
||||
|
||||
@@ -3233,7 +3259,7 @@ async def update_countries(
|
||||
try:
|
||||
await add_user_to_servers(db, added_server_ids)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка обновления счётчика серверов: {e}')
|
||||
logger.error('Ошибка обновления счётчика серверов', error=e)
|
||||
|
||||
# Update connected squads
|
||||
user.subscription.connected_squads = selected_countries
|
||||
@@ -3248,7 +3274,7 @@ async def update_countries(
|
||||
else:
|
||||
await subscription_service.create_remnawave_user(db, user.subscription)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to sync countries with RemnaWave: {e}')
|
||||
logger.error('Failed to sync countries with RemnaWave', error=e)
|
||||
|
||||
await db.refresh(user.subscription)
|
||||
|
||||
@@ -3583,7 +3609,7 @@ async def get_devices(
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching devices: {e}')
|
||||
logger.error('Error fetching devices', error=e)
|
||||
return {
|
||||
'devices': [],
|
||||
'total': 0,
|
||||
@@ -3627,7 +3653,7 @@ async def delete_device(
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error deleting device: {e}')
|
||||
logger.error('Error deleting device', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to delete device',
|
||||
@@ -3686,7 +3712,7 @@ async def delete_all_devices(
|
||||
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
|
||||
deleted_count += 1
|
||||
except Exception as device_error:
|
||||
logger.error(f'Error deleting device {device_hwid}: {device_error}')
|
||||
logger.error('Error deleting device', device_hwid=device_hwid, device_error=device_error)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
@@ -3695,7 +3721,7 @@ async def delete_all_devices(
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error deleting all devices: {e}')
|
||||
logger.error('Error deleting all devices', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to delete devices',
|
||||
@@ -3769,7 +3795,7 @@ async def get_device_reduction_info(
|
||||
if response and 'response' in response:
|
||||
connected_devices_count = response['response'].get('total', 0)
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting connected devices count: {e}')
|
||||
logger.error('Error getting connected devices count', error=e)
|
||||
|
||||
can_reduce = current_device_limit - min_device_limit
|
||||
|
||||
@@ -3853,8 +3879,11 @@ async def reduce_devices(
|
||||
if connected_devices_count > new_device_limit:
|
||||
devices_to_remove = connected_devices_count - new_device_limit
|
||||
logger.info(
|
||||
f'Removing {devices_to_remove} excess devices for user {user.id}: '
|
||||
f'had {connected_devices_count}, new limit {new_device_limit}'
|
||||
'Removing excess devices for user had new limit',
|
||||
devices_to_remove=devices_to_remove,
|
||||
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
|
||||
@@ -3871,11 +3900,11 @@ async def reduce_devices(
|
||||
delete_data = {'userUuid': user.remnawave_uuid, 'hwid': device_hwid}
|
||||
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
|
||||
devices_removed_count += 1
|
||||
logger.info(f'Removed device {device_hwid} for user {user.id}')
|
||||
logger.info('Removed device for user', device_hwid=device_hwid, user_id=user.id)
|
||||
except Exception as del_error:
|
||||
logger.error(f'Error removing device {device_hwid}: {del_error}')
|
||||
logger.error('Error removing device', device_hwid=device_hwid, del_error=del_error)
|
||||
except Exception as e:
|
||||
logger.error(f'Error checking/removing devices: {e}')
|
||||
logger.error('Error checking/removing devices', error=e)
|
||||
|
||||
old_device_limit = current_device_limit
|
||||
|
||||
@@ -3889,7 +3918,7 @@ async def reduce_devices(
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
except Exception as e:
|
||||
logger.error(f'Error updating RemnaWave user: {e}')
|
||||
logger.error('Error updating RemnaWave user', error=e)
|
||||
|
||||
logger.info(
|
||||
f'User {user.id} reduced device limit from {old_device_limit} to {new_device_limit}'
|
||||
@@ -4323,7 +4352,7 @@ async def switch_tariff(
|
||||
else:
|
||||
await subscription_service.create_remnawave_user(db, user.subscription)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to sync tariff switch with RemnaWave: {e}')
|
||||
logger.error('Failed to sync tariff switch with RemnaWave', error=e)
|
||||
|
||||
# Reset all devices on tariff switch
|
||||
devices_reset = False
|
||||
@@ -4333,9 +4362,9 @@ async def switch_tariff(
|
||||
async with service.get_api_client() as api:
|
||||
await api.reset_user_devices(user.remnawave_uuid)
|
||||
devices_reset = True
|
||||
logger.info(f'Reset all devices for user {user.id} on tariff switch')
|
||||
logger.info('Reset all devices for user on tariff switch', user_id=user.id)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to reset devices on tariff switch: {e}')
|
||||
logger.error('Failed to reset devices on tariff switch', error=e)
|
||||
|
||||
await db.refresh(user)
|
||||
await db.refresh(user.subscription)
|
||||
@@ -4363,7 +4392,7 @@ async def switch_tariff(
|
||||
finally:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send admin notification for tariff switch: {e}')
|
||||
logger.error('Failed to send admin notification for tariff switch', error=e)
|
||||
|
||||
response = {
|
||||
'success': True,
|
||||
@@ -4462,7 +4491,7 @@ async def toggle_subscription_pause(
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.enable_remnawave_user(user.remnawave_uuid)
|
||||
except Exception as e:
|
||||
logger.error(f'Error enabling RemnaWave user on resume: {e}')
|
||||
logger.error('Error enabling RemnaWave user on resume', error=e)
|
||||
|
||||
if new_paused_state:
|
||||
message = 'Daily subscription paused'
|
||||
@@ -4596,7 +4625,7 @@ async def switch_traffic_package(
|
||||
else:
|
||||
await subscription_service.create_remnawave_user(db, user.subscription)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to sync traffic switch with RemnaWave: {e}')
|
||||
logger.error('Failed to sync traffic switch with RemnaWave', error=e)
|
||||
|
||||
await db.refresh(user)
|
||||
await db.refresh(user.subscription)
|
||||
@@ -4737,7 +4766,7 @@ async def refresh_traffic(
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error refreshing traffic for user {user.id}: {e}')
|
||||
logger.error('Error refreshing traffic for user', user_id=user.id, error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to refresh traffic data',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Ticket notifications routes for cabinet."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/tickets/notifications', tags=['Cabinet Ticket Notifications'])
|
||||
admin_router = APIRouter(prefix='/admin/tickets/notifications', tags=['Cabinet Admin Ticket Notifications'])
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Support tickets routes for cabinet."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import desc, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -26,7 +26,7 @@ from ..schemas.tickets import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/tickets', tags=['Cabinet Tickets'])
|
||||
|
||||
@@ -164,7 +164,7 @@ async def create_ticket(
|
||||
try:
|
||||
await notify_admins_about_new_ticket(ticket, db)
|
||||
except Exception as e:
|
||||
logger.error(f'Error notifying admins about new ticket from cabinet: {e}')
|
||||
logger.error('Error notifying admins about new ticket from cabinet', error=e)
|
||||
|
||||
# Уведомить админов в кабинете
|
||||
try:
|
||||
@@ -173,7 +173,7 @@ async def create_ticket(
|
||||
# Отправить WebSocket уведомление
|
||||
await notify_admins_new_ticket(ticket.id, ticket.title, user.id)
|
||||
except Exception as e:
|
||||
logger.error(f'Error creating cabinet notification for new ticket: {e}')
|
||||
logger.error('Error creating cabinet notification for new ticket', error=e)
|
||||
|
||||
messages = [_message_to_response(m) for m in ticket.messages]
|
||||
|
||||
@@ -284,7 +284,7 @@ async def add_ticket_message(
|
||||
try:
|
||||
await notify_admins_about_ticket_reply(ticket, request.message, db)
|
||||
except Exception as e:
|
||||
logger.error(f'Error notifying admins about ticket reply from cabinet: {e}')
|
||||
logger.error('Error notifying admins about ticket reply from cabinet', error=e)
|
||||
|
||||
# Уведомить админов в кабинете
|
||||
try:
|
||||
@@ -295,6 +295,6 @@ async def add_ticket_message(
|
||||
# Отправить WebSocket уведомление
|
||||
await notify_admins_ticket_reply(ticket.id, (request.message or '')[:100], user.id)
|
||||
except Exception as e:
|
||||
logger.error(f'Error creating cabinet notification for user reply: {e}')
|
||||
logger.error('Error creating cabinet notification for user reply', error=e)
|
||||
|
||||
return _message_to_response(message)
|
||||
|
||||
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -42,10 +42,10 @@ class CabinetConnectionManager:
|
||||
self._admin_connections[user_id].add(websocket)
|
||||
|
||||
logger.debug(
|
||||
'Cabinet WS connected: user_id=%d, is_admin=%s, total_users=%d',
|
||||
user_id,
|
||||
is_admin,
|
||||
len(self._user_connections),
|
||||
'Cabinet WS connected: user_id is_admin total_users',
|
||||
user_id=user_id,
|
||||
is_admin=is_admin,
|
||||
user_connections_count=len(self._user_connections),
|
||||
)
|
||||
|
||||
async def disconnect(self, websocket: WebSocket, user_id: int) -> None:
|
||||
@@ -61,7 +61,7 @@ class CabinetConnectionManager:
|
||||
if not self._admin_connections[user_id]:
|
||||
del self._admin_connections[user_id]
|
||||
|
||||
logger.debug('Cabinet WS disconnected: user_id=%d', user_id)
|
||||
logger.debug('Cabinet WS disconnected: user_id', user_id=user_id)
|
||||
|
||||
async def send_to_user(self, user_id: int, message: dict) -> None:
|
||||
"""Отправить сообщение конкретному пользователю."""
|
||||
@@ -79,7 +79,7 @@ class CabinetConnectionManager:
|
||||
try:
|
||||
await ws.send_text(data)
|
||||
except Exception as e:
|
||||
logger.warning('Failed to send to user %d: %s', user_id, e)
|
||||
logger.warning('Failed to send to user', user_id=user_id, e=e)
|
||||
disconnected.add(ws)
|
||||
|
||||
# Cleanup disconnected
|
||||
@@ -105,7 +105,7 @@ class CabinetConnectionManager:
|
||||
try:
|
||||
await ws.send_text(data)
|
||||
except Exception as e:
|
||||
logger.warning('Failed to send to admin %d: %s', user_id, e)
|
||||
logger.warning('Failed to send to admin', user_id=user_id, e=e)
|
||||
if user_id not in disconnected_by_user:
|
||||
disconnected_by_user[user_id] = set()
|
||||
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
|
||||
except (TimeoutError, OSError, ConnectionRefusedError) as e:
|
||||
logger.error('Database connection error in WS token verification: %s', str(e)[:200])
|
||||
logger.error('Database connection error in WS token verification', e=str(e)[:200])
|
||||
return None, False
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
|
||||
token = websocket.query_params.get('token')
|
||||
|
||||
if not token:
|
||||
logger.debug('Cabinet WS: No token from %s', client_host)
|
||||
logger.debug('Cabinet WS: No token from', client_host=client_host)
|
||||
# Принимаем и сразу закрываем с кодом ошибки
|
||||
await websocket.accept()
|
||||
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)
|
||||
|
||||
if not user_id:
|
||||
logger.debug('Cabinet WS: Invalid token from %s', client_host)
|
||||
logger.debug('Cabinet WS: Invalid token from', client_host=client_host)
|
||||
# Принимаем и сразу закрываем с кодом ошибки
|
||||
await websocket.accept()
|
||||
await websocket.close(code=1008, reason='Unauthorized: Invalid token')
|
||||
@@ -184,9 +184,9 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
|
||||
# Принимаем соединение
|
||||
try:
|
||||
await websocket.accept()
|
||||
logger.debug('Cabinet WS accepted: user_id=%d, is_admin=%s', user_id, is_admin)
|
||||
logger.debug('Cabinet WS accepted: user_id is_admin', user_id=user_id, is_admin=is_admin)
|
||||
except Exception as e:
|
||||
logger.error('Cabinet WS: Failed to accept from %s: %s', client_host, e)
|
||||
logger.error('Cabinet WS: Failed to accept from', client_host=client_host, e=e)
|
||||
return
|
||||
|
||||
# Регистрируем подключение
|
||||
@@ -213,17 +213,17 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.send_json({'type': 'pong'})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.warning('Cabinet WS: Invalid JSON from user %d', user_id)
|
||||
logger.warning('Cabinet WS: Invalid JSON from user', user_id=user_id)
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.exception('Cabinet WS error for user %d: %s', user_id, e)
|
||||
logger.exception('Cabinet WS error for user', user_id=user_id, e=e)
|
||||
break
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.debug('Cabinet WS disconnected: user_id=%d', user_id)
|
||||
logger.debug('Cabinet WS disconnected: user_id', user_id=user_id)
|
||||
except Exception as e:
|
||||
logger.exception('Cabinet WS error: %s', e)
|
||||
logger.exception('Cabinet WS error', e=e)
|
||||
finally:
|
||||
await cabinet_ws_manager.disconnect(websocket, user_id)
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
API роуты колеса удачи для пользователей.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -32,7 +32,7 @@ from app.database.models import User
|
||||
from app.services.wheel_service import wheel_service
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/wheel', tags=['Fortune Wheel'])
|
||||
|
||||
@@ -253,14 +253,16 @@ async def create_stars_invoice(
|
||||
result = response.json()
|
||||
|
||||
if not result.get('ok'):
|
||||
logger.error(f'Telegram API error: {result}')
|
||||
logger.error('Telegram API error', result=result)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Ошибка создания инвойса',
|
||||
)
|
||||
|
||||
invoice_url = result['result']
|
||||
logger.info(f'Created Stars invoice for wheel spin: user={user.id}, stars={stars_amount}')
|
||||
logger.info(
|
||||
'Created Stars invoice for wheel spin: user=, stars', user_id=user.id, stars_amount=stars_amount
|
||||
)
|
||||
|
||||
return StarsInvoiceResponse(
|
||||
invoice_url=invoice_url,
|
||||
@@ -268,7 +270,7 @@ async def create_stars_invoice(
|
||||
)
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f'HTTP error creating invoice: {e}')
|
||||
logger.error('HTTP error creating invoice', error=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Ошибка соединения с Telegram',
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
"""Email service for sending verification and password reset emails."""
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import structlog
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class EmailService:
|
||||
@@ -41,7 +42,7 @@ class EmailService:
|
||||
if smtp.has_extn('auth'):
|
||||
smtp.login(self.user, self.password)
|
||||
else:
|
||||
logger.debug(f'SMTP server {self.host} does not support AUTH, skipping authentication')
|
||||
logger.debug('SMTP server does not support AUTH, skipping authentication', host=self.host)
|
||||
|
||||
return smtp
|
||||
|
||||
@@ -94,11 +95,11 @@ class EmailService:
|
||||
with self._get_smtp_connection() as smtp:
|
||||
smtp.sendmail(self.from_email, to_email, msg.as_string())
|
||||
|
||||
logger.info(f'Email sent successfully to {to_email}')
|
||||
logger.info('Email sent successfully to', to_email=to_email)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send email to {to_email}: {e}')
|
||||
logger.error('Failed to send email to', to_email=to_email, error=e)
|
||||
return False
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.database import AsyncSessionLocal
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def get_template_override(
|
||||
@@ -56,7 +56,9 @@ async def get_template_override(
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug('Не удалось получить override шаблона %s/%s: %s', notification_type, language, e)
|
||||
logger.debug(
|
||||
'Не удалось получить override шаблона /', notification_type=notification_type, language=language, e=e
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+16
-33
@@ -1,7 +1,6 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import html
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
@@ -11,6 +10,7 @@ from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import structlog
|
||||
from pydantic import Field, field_validator
|
||||
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}$')
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -1297,7 +1297,7 @@ class Settings(BaseSettings):
|
||||
raise ValueError
|
||||
return time(hour=hours, minute=minutes)
|
||||
except (ValueError, AttributeError):
|
||||
logging.getLogger(__name__).warning('Некорректное значение ADMIN_REPORTS_SEND_TIME: %s', value)
|
||||
logger.warning('Некорректное значение ADMIN_REPORTS_SEND_TIME', send_time_value=value)
|
||||
return None
|
||||
|
||||
def kopeks_to_rubles(self, kopeks: int) -> float:
|
||||
@@ -1317,17 +1317,14 @@ class Settings(BaseSettings):
|
||||
|
||||
if len(cleaned) > 16:
|
||||
logger.warning(
|
||||
'Некорректная длина %s: максимум 16 символов, получено %s',
|
||||
setting_name,
|
||||
len(cleaned),
|
||||
'Некорректная длина : максимум 16 символов, получено',
|
||||
setting_name=setting_name,
|
||||
cleaned_count=len(cleaned),
|
||||
)
|
||||
return None
|
||||
|
||||
if not USER_TAG_PATTERN.fullmatch(cleaned):
|
||||
logger.warning(
|
||||
'Некорректный формат %s: допустимы только A-Z, 0-9 и подчёркивание',
|
||||
setting_name,
|
||||
)
|
||||
logger.warning('Некорректный формат : допустимы только A-Z, 0-9 и подчёркивание', setting_name=setting_name)
|
||||
return None
|
||||
|
||||
return cleaned
|
||||
@@ -1448,9 +1445,9 @@ class Settings(BaseSettings):
|
||||
try:
|
||||
return int(self.EXTERNAL_ADMIN_TOKEN_BOT_ID) if self.EXTERNAL_ADMIN_TOKEN_BOT_ID else None
|
||||
except (TypeError, ValueError): # pragma: no cover - защитная ветка для некорректных значений
|
||||
logging.getLogger(__name__).warning(
|
||||
'Некорректный идентификатор бота для внешней админки: %s',
|
||||
self.EXTERNAL_ADMIN_TOKEN_BOT_ID,
|
||||
logger.warning(
|
||||
'Некорректный идентификатор бота для внешней админки',
|
||||
EXTERNAL_ADMIN_TOKEN_BOT_ID=self.EXTERNAL_ADMIN_TOKEN_BOT_ID,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -1531,10 +1528,7 @@ class Settings(BaseSettings):
|
||||
try:
|
||||
value = int(raw_value)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
'Некорректное значение DEVICES_SELECTION_DISABLED_AMOUNT: %s',
|
||||
raw_value,
|
||||
)
|
||||
logger.warning('Некорректное значение DEVICES_SELECTION_DISABLED_AMOUNT', raw_value=raw_value)
|
||||
return None
|
||||
|
||||
if value < 0:
|
||||
@@ -1574,8 +1568,7 @@ class Settings(BaseSettings):
|
||||
value = int(self.TRIAL_ACTIVATION_PRICE)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
'Некорректное значение TRIAL_ACTIVATION_PRICE: %s',
|
||||
self.TRIAL_ACTIVATION_PRICE,
|
||||
'Некорректное значение TRIAL_ACTIVATION_PRICE', TRIAL_ACTIVATION_PRICE=self.TRIAL_ACTIVATION_PRICE
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -1694,7 +1687,7 @@ class Settings(BaseSettings):
|
||||
try:
|
||||
method_code = int(part)
|
||||
except ValueError:
|
||||
logger.warning('Некорректный код метода Platega: %s', part)
|
||||
logger.warning('Некорректный код метода Platega', part=part)
|
||||
continue
|
||||
if method_code in {2, 10, 11, 12, 13} and method_code not in seen:
|
||||
methods.append(method_code)
|
||||
@@ -1789,8 +1782,8 @@ class Settings(BaseSettings):
|
||||
|
||||
if minutes <= 0:
|
||||
logger.warning(
|
||||
'Некорректный интервал автопроверки платежей: %s. Используется значение по умолчанию 10 минут.',
|
||||
self.PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES,
|
||||
'Некорректный интервал автопроверки платежей: . Используется значение по умолчанию 10 минут.',
|
||||
PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES=self.PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES,
|
||||
)
|
||||
return 10
|
||||
|
||||
@@ -2144,22 +2137,13 @@ class Settings(BaseSettings):
|
||||
return self.REFERRAL_NOTIFICATIONS_ENABLED
|
||||
|
||||
def get_traffic_packages(self) -> list[dict]:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
packages = []
|
||||
config_str = self.TRAFFIC_PACKAGES_CONFIG.strip()
|
||||
|
||||
logger.debug(f"CONFIG STRING: '{config_str}'")
|
||||
|
||||
if not config_str:
|
||||
logger.debug('CONFIG EMPTY, USING FALLBACK')
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
logger.debug('PARSING CONFIG...')
|
||||
|
||||
for package_config in config_str.split(','):
|
||||
package_config = package_config.strip()
|
||||
if not package_config:
|
||||
@@ -2178,11 +2162,10 @@ class Settings(BaseSettings):
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
logger.debug(f'PARSED {len(packages)} packages from config')
|
||||
return packages if packages else self._get_fallback_traffic_packages()
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f'ERROR PARSING CONFIG: {e}')
|
||||
logger.warning('ERROR PARSING CONFIG', error=e)
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
def is_version_check_enabled(self) -> bool:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -17,7 +17,7 @@ from app.database.models import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_campaign(
|
||||
@@ -57,10 +57,10 @@ async def create_campaign(
|
||||
await db.refresh(campaign)
|
||||
|
||||
logger.info(
|
||||
'📣 Создана рекламная кампания %s (start=%s, bonus=%s)',
|
||||
campaign.name,
|
||||
campaign.start_parameter,
|
||||
campaign.bonus_type,
|
||||
'📣 Создана рекламная кампания (start bonus=)',
|
||||
campaign_name=campaign.name,
|
||||
start_parameter=campaign.start_parameter,
|
||||
bonus_type=campaign.bonus_type,
|
||||
)
|
||||
return campaign
|
||||
|
||||
@@ -157,14 +157,14 @@ async def update_campaign(
|
||||
await db.commit()
|
||||
await db.refresh(campaign)
|
||||
|
||||
logger.info('✏️ Обновлена рекламная кампания %s (%s)', campaign.name, update_data)
|
||||
logger.info('✏️ Обновлена рекламная кампания', campaign_name=campaign.name, update_data=update_data)
|
||||
return campaign
|
||||
|
||||
|
||||
async def delete_campaign(db: AsyncSession, campaign: AdvertisingCampaign) -> bool:
|
||||
await db.execute(delete(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id))
|
||||
await db.commit()
|
||||
logger.info('🗑️ Удалена рекламная кампания %s', campaign.name)
|
||||
logger.info('🗑️ Удалена рекламная кампания', campaign_name=campaign.name)
|
||||
return True
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ async def record_campaign_registration(
|
||||
await db.commit()
|
||||
await db.refresh(registration)
|
||||
|
||||
logger.info('📈 Регистрируем пользователя %s в кампании %s', user_id, campaign_id)
|
||||
logger.info('📈 Регистрируем пользователя в кампании', user_id=user_id, campaign_id=campaign_id)
|
||||
return registration
|
||||
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import CloudPaymentsPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_cloudpayments_payment(
|
||||
@@ -65,10 +65,10 @@ async def create_cloudpayments_payment(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.debug(
|
||||
'Created CloudPayments payment: id=%s, invoice=%s, amount=%s',
|
||||
payment.id,
|
||||
invoice_id,
|
||||
amount_kopeks,
|
||||
'Created CloudPayments payment: id invoice amount',
|
||||
payment_id=payment.id,
|
||||
invoice_id=invoice_id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -195,9 +195,7 @@ async def mark_cloudpayments_payment_as_paid(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Marked CloudPayments payment as paid: id=%s, invoice=%s',
|
||||
payment.id,
|
||||
payment.invoice_id,
|
||||
'Marked CloudPayments payment as paid: id invoice', payment_id=payment.id, invoice_id=payment.invoice_id
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, delete, desc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import ContestAttempt, ContestRound, ContestTemplate, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# Templates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import CryptoBotPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_cryptobot_payment(
|
||||
@@ -41,7 +41,13 @@ async def create_cryptobot_payment(
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(f'Создан CryptoBot платеж: {invoice_id} на {amount} {asset} для пользователя {user_id}')
|
||||
logger.info(
|
||||
'Создан CryptoBot платеж: на для пользователя',
|
||||
invoice_id=invoice_id,
|
||||
amount=amount,
|
||||
asset=asset,
|
||||
user_id=user_id,
|
||||
)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -78,7 +84,7 @@ async def update_cryptobot_payment_status(
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(f'Обновлен статус CryptoBot платежа {invoice_id}: {status}')
|
||||
logger.info('Обновлен статус CryptoBot платежа', invoice_id=invoice_id, status=status)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -96,7 +102,7 @@ async def link_cryptobot_payment_to_transaction(
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(f'Связан CryptoBot платеж {invoice_id} с транзакцией {transaction_id}')
|
||||
logger.info('Связан CryptoBot платеж с транзакцией', invoice_id=invoice_id, transaction_id=transaction_id)
|
||||
return payment
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def upsert_discount_offer(
|
||||
@@ -178,17 +178,12 @@ async def mark_offer_claimed(
|
||||
details=details,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to record promo offer claim log for offer %s: %s',
|
||||
offer.id,
|
||||
exc,
|
||||
)
|
||||
logger.warning('Failed to record promo offer claim log for offer', offer_id=offer.id, exc=exc)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception as rollback_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to rollback session after promo offer claim log failure: %s',
|
||||
rollback_error,
|
||||
'Failed to rollback session after promo offer claim log failure', rollback_error=rollback_error
|
||||
)
|
||||
|
||||
return offer
|
||||
@@ -239,16 +234,13 @@ async def deactivate_expired_offers(db: AsyncSession) -> int:
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to record promo offer disable log for offer %s: %s',
|
||||
payload.get('offer_id'),
|
||||
exc,
|
||||
'Failed to record promo offer disable log for offer', payload=payload.get('offer_id'), exc=exc
|
||||
)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception as rollback_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to rollback session after promo offer disable log failure: %s',
|
||||
rollback_error,
|
||||
'Failed to rollback session after promo offer disable log failure', rollback_error=rollback_error
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import FaqPage, FaqSetting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
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.refresh(page)
|
||||
|
||||
logger.info('✅ Создана страница FAQ %s для языка %s', page.id, language)
|
||||
logger.info('✅ Создана страница FAQ для языка', page_id=page.id, language=language)
|
||||
|
||||
return page
|
||||
|
||||
@@ -122,7 +122,7 @@ async def update_faq_page(
|
||||
await db.commit()
|
||||
await db.refresh(page)
|
||||
|
||||
logger.info('✅ Страница FAQ %s обновлена', page.id)
|
||||
logger.info('✅ Страница FAQ обновлена', page_id=page.id)
|
||||
|
||||
return page
|
||||
|
||||
@@ -130,7 +130,7 @@ async def update_faq_page(
|
||||
async def delete_faq_page(db: AsyncSession, page_id: int) -> None:
|
||||
await db.execute(delete(FaqPage).where(FaqPage.id == page_id))
|
||||
await db.commit()
|
||||
logger.info('🗑️ Страница FAQ %s удалена', page_id)
|
||||
logger.info('🗑️ Страница FAQ удалена', page_id=page_id)
|
||||
|
||||
|
||||
async def bulk_update_order(
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""CRUD операции для платежей Freekassa."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import FreekassaPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_freekassa_payment(
|
||||
@@ -41,7 +41,7 @@ async def create_freekassa_payment(
|
||||
db.add(payment)
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
logger.info(f'Создан платеж Freekassa: order_id={order_id}, user_id={user_id}')
|
||||
logger.info('Создан платеж Freekassa: order_id=, user_id', order_id=order_id, user_id=user_id)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -92,7 +92,12 @@ async def update_freekassa_payment_status(
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
logger.info(f'Обновлен статус платежа Freekassa: order_id={payment.order_id}, status={status}, is_paid={is_paid}')
|
||||
logger.info(
|
||||
'Обновлен статус платежа Freekassa: order_id=, status=, is_paid',
|
||||
order_id=payment.order_id,
|
||||
status=status,
|
||||
is_paid=is_paid,
|
||||
)
|
||||
return payment
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import HeleketPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_heleket_payment(
|
||||
@@ -50,12 +50,12 @@ async def create_heleket_payment(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Создан Heleket платеж: uuid=%s order_id=%s amount=%s %s для пользователя %s',
|
||||
uuid,
|
||||
order_id,
|
||||
amount,
|
||||
currency,
|
||||
user_id,
|
||||
'Создан Heleket платеж: uuid= order_id= amount= для пользователя',
|
||||
uuid=uuid,
|
||||
order_id=order_id,
|
||||
amount=amount,
|
||||
currency=currency,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -107,7 +107,7 @@ async def update_heleket_payment(
|
||||
payment = await get_heleket_payment_by_uuid(db, uuid)
|
||||
|
||||
if not payment:
|
||||
logger.error('Heleket платеж с uuid=%s не найден', uuid)
|
||||
logger.error('Heleket платеж с uuid= не найден', uuid=uuid)
|
||||
return None
|
||||
|
||||
if status is not None:
|
||||
@@ -135,11 +135,11 @@ async def update_heleket_payment(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Обновлен Heleket платеж %s: статус=%s payer_amount=%s %s',
|
||||
uuid,
|
||||
payment.status,
|
||||
payment.payer_amount,
|
||||
payment.payer_currency,
|
||||
'Обновлен Heleket платеж : статус= payer_amount',
|
||||
uuid=uuid,
|
||||
payment_status=payment.status,
|
||||
payer_amount=payment.payer_amount,
|
||||
payer_currency=payment.payer_currency,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -153,7 +153,7 @@ async def link_heleket_payment_to_transaction(
|
||||
payment = await get_heleket_payment_by_uuid(db, uuid)
|
||||
|
||||
if not payment:
|
||||
logger.error('Не найден Heleket платеж для связи с транзакцией: %s', uuid)
|
||||
logger.error('Не найден Heleket платеж для связи с транзакцией', uuid=uuid)
|
||||
return None
|
||||
|
||||
payment.transaction_id = transaction_id
|
||||
@@ -162,10 +162,6 @@ async def link_heleket_payment_to_transaction(
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Heleket платеж %s связан с транзакцией %s',
|
||||
uuid,
|
||||
transaction_id,
|
||||
)
|
||||
logger.info('Heleket платеж связан с транзакцией', uuid=uuid, transaction_id=transaction_id)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""CRUD операции для платежей KassaAI."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import KassaAiPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_kassa_ai_payment(
|
||||
@@ -43,7 +43,7 @@ async def create_kassa_ai_payment(
|
||||
db.add(payment)
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
logger.info(f'Создан платеж KassaAI: order_id={order_id}, user_id={user_id}')
|
||||
logger.info('Создан платеж KassaAI: order_id=, user_id', order_id=order_id, user_id=user_id)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -94,7 +94,12 @@ async def update_kassa_ai_payment_status(
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
logger.info(f'Обновлен статус платежа KassaAI: order_id={payment.order_id}, status={status}, is_paid={is_paid}')
|
||||
logger.info(
|
||||
'Обновлен статус платежа KassaAI: order_id=, status=, is_paid',
|
||||
order_id=payment.order_id,
|
||||
status=status,
|
||||
is_paid=is_paid,
|
||||
)
|
||||
return payment
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -8,7 +8,7 @@ from app.config import settings
|
||||
from app.database.models import MulenPayPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_mulenpay_payment(
|
||||
@@ -41,12 +41,12 @@ async def create_mulenpay_payment(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Создан %s платеж #%s (uuid=%s) на сумму %s копеек для пользователя %s',
|
||||
settings.get_mulenpay_display_name(),
|
||||
payment.mulen_payment_id,
|
||||
uuid,
|
||||
amount_kopeks,
|
||||
user_id,
|
||||
'Создан платеж # (uuid=) на сумму копеек для пользователя',
|
||||
get_mulenpay_display_name=settings.get_mulenpay_display_name(),
|
||||
mulen_payment_id=payment.mulen_payment_id,
|
||||
uuid=uuid,
|
||||
amount_kopeks=amount_kopeks,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import SentNotification
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def notification_sent(
|
||||
|
||||
+12
-16
@@ -2,17 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import Pal24Payment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_pal24_payment(
|
||||
@@ -51,11 +51,11 @@ async def create_pal24_payment(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Создан Pal24 платеж #%s для пользователя %s: %s копеек (статус %s)',
|
||||
payment.id,
|
||||
user_id,
|
||||
amount_kopeks,
|
||||
status,
|
||||
'Создан Pal24 платеж # для пользователя : копеек (статус)',
|
||||
payment_id=payment.id,
|
||||
user_id=user_id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
status=status,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -128,10 +128,10 @@ async def update_pal24_payment_status(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Обновлен Pal24 платеж %s: статус=%s, is_paid=%s',
|
||||
payment.bill_id,
|
||||
payment.status,
|
||||
payment.is_paid,
|
||||
'Обновлен Pal24 платеж : статус is_paid',
|
||||
bill_id=payment.bill_id,
|
||||
payment_status=payment.status,
|
||||
is_paid=payment.is_paid,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -145,9 +145,5 @@ async def link_pal24_payment_to_transaction(
|
||||
await db.execute(update(Pal24Payment).where(Pal24Payment.id == payment.id).values(transaction_id=transaction_id))
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
logger.info(
|
||||
'Pal24 платеж %s привязан к транзакции %s',
|
||||
payment.bill_id,
|
||||
transaction_id,
|
||||
)
|
||||
logger.info('Pal24 платеж привязан к транзакции', bill_id=payment.bill_id, transaction_id=transaction_id)
|
||||
return payment
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import PlategaPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_platega_payment(
|
||||
@@ -55,11 +55,11 @@ async def create_platega_payment(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Создан Platega платеж #%s (tx=%s) на сумму %s копеек для пользователя %s',
|
||||
payment.id,
|
||||
platega_transaction_id,
|
||||
amount_kopeks,
|
||||
user_id,
|
||||
'Создан Platega платеж # (tx=) на сумму копеек для пользователя',
|
||||
payment_id=payment.id,
|
||||
platega_transaction_id=platega_transaction_id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -14,7 +14,7 @@ from app.database.models import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_poll(
|
||||
@@ -97,7 +97,7 @@ async def delete_poll(db: AsyncSession, poll_id: int) -> bool:
|
||||
|
||||
await db.delete(poll)
|
||||
await db.commit()
|
||||
logger.info('🗑️ Удалён опрос %s', poll_id)
|
||||
logger.info('🗑️ Удалён опрос', poll_id=poll_id)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import PrivacyPolicy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def get_privacy_policy(db: AsyncSession, language: str) -> PrivacyPolicy | None:
|
||||
@@ -38,11 +38,7 @@ async def upsert_privacy_policy(
|
||||
await db.commit()
|
||||
await db.refresh(policy)
|
||||
|
||||
logger.info(
|
||||
'✅ Политика конфиденциальности для языка %s обновлена (ID: %s)',
|
||||
language,
|
||||
policy.id,
|
||||
)
|
||||
logger.info('✅ Политика конфиденциальности для языка обновлена (ID:)', language=language, policy_id=policy.id)
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -25,7 +24,7 @@ def _normalize_period_discounts(period_discounts: dict[int, int] | None) -> dict
|
||||
return normalized
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def get_promo_groups_with_counts(
|
||||
@@ -194,11 +193,7 @@ async def update_promo_group(
|
||||
await db.commit()
|
||||
await db.refresh(group)
|
||||
|
||||
logger.info(
|
||||
"Обновлена промогруппа '%s' (id=%s)",
|
||||
group.name,
|
||||
group.id,
|
||||
)
|
||||
logger.info("Обновлена промогруппа '' (id=)", group_name=group.name, group_id=group.id)
|
||||
return group
|
||||
|
||||
|
||||
@@ -246,10 +241,10 @@ async def delete_promo_group(db: AsyncSession, group: PromoGroup) -> bool:
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"Промогруппа '%s' (id=%s) удалена, пользователи переведены в '%s'",
|
||||
group.name,
|
||||
group.id,
|
||||
default_group.name,
|
||||
"Промогруппа '' (id=) удалена, пользователи переведены в ''",
|
||||
group_name=group.name,
|
||||
group_id=group.id,
|
||||
default_group_name=default_group.name,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -9,7 +8,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import PromoOfferLog
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def log_promo_offer_action(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import PromoCode, PromoCodeType, PromoCodeUse, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def get_promocode_by_code(db: AsyncSession, code: str) -> PromoCode | None:
|
||||
@@ -78,9 +78,9 @@ async def create_promocode(
|
||||
await db.refresh(promocode)
|
||||
|
||||
if promo_group_id:
|
||||
logger.info(f'✅ Создан промокод: {code} с промогруппой ID {promo_group_id}')
|
||||
logger.info('✅ Создан промокод: с промогруппой ID', code=code, promo_group_id=promo_group_id)
|
||||
else:
|
||||
logger.info(f'✅ Создан промокод: {code}')
|
||||
logger.info('✅ Создан промокод', code=code)
|
||||
return promocode
|
||||
|
||||
|
||||
@@ -97,11 +97,11 @@ async def use_promocode(db: AsyncSession, promocode_id: int, user_id: int) -> bo
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'✅ Промокод {promocode.code} использован пользователем {user_id}')
|
||||
logger.info('✅ Промокод использован пользователем', code=promocode.code, user_id=user_id)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка использования промокода: {e}')
|
||||
logger.error('Ошибка использования промокода', error=e)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -120,7 +120,7 @@ async def create_promocode_use(db: AsyncSession, promocode_id: int, user_id: int
|
||||
await db.commit()
|
||||
await db.refresh(promocode_use)
|
||||
|
||||
logger.info(f'📝 Записано использование промокода {promocode_id} пользователем {user_id}')
|
||||
logger.info('📝 Записано использование промокода пользователем', promocode_id=promocode_id, user_id=user_id)
|
||||
return promocode_use
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@ async def delete_promocode(db: AsyncSession, promocode: PromoCode) -> bool:
|
||||
await db.delete(promocode)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'🗑️ Удален промокод: {promocode.code}')
|
||||
logger.info('🗑️ Удален промокод', code=promocode.code)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка удаления промокода: {e}')
|
||||
logger.error('Ошибка удаления промокода', error=e)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import PublicOffer
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def get_public_offer(db: AsyncSession, language: str) -> PublicOffer | None:
|
||||
@@ -38,11 +38,7 @@ async def upsert_public_offer(
|
||||
await db.commit()
|
||||
await db.refresh(offer)
|
||||
|
||||
logger.info(
|
||||
'✅ Публичная оферта для языка %s обновлена (ID: %s)',
|
||||
language,
|
||||
offer.id,
|
||||
)
|
||||
logger.info('✅ Публичная оферта для языка обновлена (ID:)', language=language, offer_id=offer.id)
|
||||
|
||||
return offer
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import ReferralEarning, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_referral_earning(
|
||||
@@ -31,7 +31,9 @@ async def create_referral_earning(
|
||||
await db.commit()
|
||||
await db.refresh(earning)
|
||||
|
||||
logger.info(f'💰 Создан реферальный заработок: {amount_kopeks / 100}₽ для пользователя {user_id}')
|
||||
logger.info(
|
||||
'💰 Создан реферальный заработок: ₽ для пользователя', amount_kopeks=amount_kopeks / 100, user_id=user_id
|
||||
)
|
||||
return earning
|
||||
|
||||
|
||||
@@ -211,7 +213,10 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
|
||||
month_earnings = month_referral_earnings_result.scalar() + month_transaction_earnings_result.scalar()
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import date, datetime, time
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, desc, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -16,7 +16,7 @@ from app.database.models import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_referral_contest(
|
||||
@@ -725,7 +725,12 @@ async def sync_contest_events(
|
||||
# Конец дня: 23:59:59.999999
|
||||
contest_end = contest_end.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
|
||||
logger.info('Синхронизация конкурса %s: период с %s по %s', contest_id, contest_start, contest_end)
|
||||
logger.info(
|
||||
'Синхронизация конкурса : период с по',
|
||||
contest_id=contest_id,
|
||||
contest_start=contest_start,
|
||||
contest_end=contest_end,
|
||||
)
|
||||
|
||||
stats = {
|
||||
'updated': 0,
|
||||
@@ -815,7 +820,11 @@ async def sync_contest_events(
|
||||
# Логируем значительные изменения
|
||||
if abs(old_amount - total_paid) > 10000: # больше 100 руб разницы
|
||||
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:
|
||||
stats['skipped'] += 1
|
||||
@@ -824,11 +833,11 @@ async def sync_contest_events(
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
'Синхронизация конкурса %s завершена: обновлено %s, пропущено %s, сумма %s коп.',
|
||||
contest_id,
|
||||
stats['updated'],
|
||||
stats['skipped'],
|
||||
stats['total_amount'],
|
||||
'Синхронизация конкурса завершена: обновлено , пропущено , сумма коп.',
|
||||
contest_id=contest_id,
|
||||
stats=stats['updated'],
|
||||
stats_2=stats['skipped'],
|
||||
stats_3=stats['total_amount'],
|
||||
)
|
||||
|
||||
return stats
|
||||
@@ -861,7 +870,9 @@ async def cleanup_invalid_contest_events(
|
||||
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)
|
||||
|
||||
logger.info('Очистка конкурса %s: период с %s по %s', contest_id, contest_start, contest_end)
|
||||
logger.info(
|
||||
'Очистка конкурса : период с по', contest_id=contest_id, contest_start=contest_start, contest_end=contest_end
|
||||
)
|
||||
|
||||
# Считаем сколько было событий до очистки
|
||||
total_before_result = await db.execute(
|
||||
@@ -905,11 +916,11 @@ async def cleanup_invalid_contest_events(
|
||||
remaining = int(remaining_result.scalar_one() or 0)
|
||||
|
||||
logger.info(
|
||||
'Очистка конкурса %s завершена: удалено %s невалидных событий, осталось %s валидных (было %s)',
|
||||
contest_id,
|
||||
deleted,
|
||||
remaining,
|
||||
total_before,
|
||||
'Очистка конкурса завершена: удалено невалидных событий, осталось валидных (было)',
|
||||
contest_id=contest_id,
|
||||
deleted=deleted,
|
||||
remaining=remaining,
|
||||
total_before=total_before,
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import ServiceRule
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
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.refresh(new_rules)
|
||||
|
||||
logger.info(f'✅ Правила для языка {language} обновлены (ID: {new_rules.id})')
|
||||
logger.info('✅ Правила для языка обновлены (ID: )', language=language, new_rules_id=new_rules.id)
|
||||
return new_rules
|
||||
|
||||
|
||||
@@ -53,12 +53,14 @@ async def clear_all_rules(db: AsyncSession, language: str = 'ru') -> bool:
|
||||
await db.commit()
|
||||
|
||||
rows_affected = result.rowcount
|
||||
logger.info(f'✅ Очищены правила для языка {language}. Деактивировано записей: {rows_affected}')
|
||||
logger.info(
|
||||
'✅ Очищены правила для языка . Деактивировано записей', language=language, rows_affected=rows_affected
|
||||
)
|
||||
|
||||
return rows_affected > 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Ошибка при очистке правил для языка {language}: {e}')
|
||||
logger.error('❌ Ошибка при очистке правил для языка', language=language, error=e)
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
@@ -102,7 +104,7 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
|
||||
rule_to_restore = result.scalar_one_or_none()
|
||||
|
||||
if not rule_to_restore:
|
||||
logger.warning(f'Правило с ID {rule_id} не найдено для языка {language}')
|
||||
logger.warning('Правило с ID не найдено для языка', rule_id=rule_id, language=language)
|
||||
return None
|
||||
|
||||
await db.execute(
|
||||
@@ -119,11 +121,13 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
|
||||
await db.commit()
|
||||
await db.refresh(restored_rule)
|
||||
|
||||
logger.info(f'✅ Восстановлена версия правил ID {rule_id} как новое правило ID {restored_rule.id}')
|
||||
logger.info(
|
||||
'✅ Восстановлена версия правил ID как новое правило ID', rule_id=rule_id, restored_rule_id=restored_rule.id
|
||||
)
|
||||
return restored_rule
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Ошибка при восстановлении правил ID {rule_id}: {e}')
|
||||
logger.error('❌ Ошибка при восстановлении правил ID', rule_id=rule_id, error=e)
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
@@ -156,5 +160,5 @@ async def get_rules_statistics(db: AsyncSession) -> dict:
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Ошибка при получении статистики правил: {e}')
|
||||
logger.error('❌ Ошибка при получении статистики правил', error=e)
|
||||
return {'total_active': 0, 'total_all_time': 0, 'languages': {}, 'total_languages': 0, 'error': str(e)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
import random
|
||||
from collections.abc import Iterable, Sequence
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
and_,
|
||||
@@ -28,7 +28,7 @@ from app.database.models import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
if len(promo_groups) != len(normalized_group_ids):
|
||||
logger.warning('Не все промогруппы найдены при создании сервера %s', display_name)
|
||||
logger.warning('Не все промогруппы найдены при создании сервера', display_name=display_name)
|
||||
|
||||
server_squad = ServerSquad(
|
||||
squad_uuid=squad_uuid,
|
||||
@@ -84,7 +84,7 @@ async def create_server_squad(
|
||||
await db.commit()
|
||||
await db.refresh(server_squad)
|
||||
|
||||
logger.info(f'✅ Создан сервер {display_name} (UUID: {squad_uuid})')
|
||||
logger.info('✅ Создан сервер (UUID: )', display_name=display_name, squad_uuid=squad_uuid)
|
||||
return server_squad
|
||||
|
||||
|
||||
@@ -267,13 +267,17 @@ async def delete_server_squad(db: AsyncSession, server_id: int) -> bool:
|
||||
connections_count = connections_result.scalar()
|
||||
|
||||
if connections_count > 0:
|
||||
logger.warning(f'⚠ Нельзя удалить сервер {server_id}: есть активные подключения ({connections_count})')
|
||||
logger.warning(
|
||||
'⚠ Нельзя удалить сервер есть активные подключения',
|
||||
server_id=server_id,
|
||||
connections_count=connections_count,
|
||||
)
|
||||
return False
|
||||
|
||||
await db.execute(delete(ServerSquad).where(ServerSquad.id == server_id))
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'🗑️ Удален сервер (ID: {server_id})')
|
||||
logger.info('🗑️ Удален сервер (ID: )', server_id=server_id)
|
||||
return True
|
||||
|
||||
|
||||
@@ -322,11 +326,7 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
|
||||
subscription_ids = {row[0] for row in subscription_ids_result.fetchall()}
|
||||
|
||||
for server in removed_servers:
|
||||
logger.info(
|
||||
'🗑️ Удаляется сервер %s (UUID: %s)',
|
||||
server.display_name,
|
||||
server.squad_uuid,
|
||||
)
|
||||
logger.info('🗑️ Удаляется сервер (UUID:)', display_name=server.display_name, squad_uuid=server.squad_uuid)
|
||||
|
||||
await db.execute(delete(SubscriptionServer).where(SubscriptionServer.server_squad_id.in_(removed_ids)))
|
||||
|
||||
@@ -386,20 +386,14 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
|
||||
removed = len(removed_servers)
|
||||
|
||||
if cleaned_subscriptions:
|
||||
logger.info(
|
||||
'🧹 Обновлены подписки после удаления серверов: %s',
|
||||
cleaned_subscriptions,
|
||||
)
|
||||
logger.info('🧹 Обновлены подписки после удаления серверов', cleaned_subscriptions=cleaned_subscriptions)
|
||||
|
||||
if cleaned_tariffs:
|
||||
logger.info(
|
||||
'🧹 Обновлены тарифы после удаления серверов: %s',
|
||||
cleaned_tariffs,
|
||||
)
|
||||
logger.info('🧹 Обновлены тарифы после удаления серверов', cleaned_tariffs=cleaned_tariffs)
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'🔄 Синхронизация завершена: +{created} ~{updated} -{removed}')
|
||||
logger.info('🔄 Синхронизация завершена: + ~', created=created, updated=updated, removed=removed)
|
||||
return created, updated, removed
|
||||
|
||||
|
||||
@@ -767,11 +761,11 @@ async def add_user_to_servers(db: AsyncSession, server_squad_ids: list[int]) ->
|
||||
)
|
||||
|
||||
await db.flush()
|
||||
logger.info(f'✅ Увеличен счетчик пользователей для серверов: {server_squad_ids}')
|
||||
logger.info('✅ Увеличен счетчик пользователей для серверов', server_squad_ids=server_squad_ids)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка увеличения счетчика пользователей: {e}')
|
||||
logger.error('Ошибка увеличения счетчика пользователей', error=e)
|
||||
raise
|
||||
|
||||
|
||||
@@ -785,11 +779,11 @@ async def remove_user_from_servers(db: AsyncSession, server_squad_ids: list[int]
|
||||
)
|
||||
|
||||
await db.flush()
|
||||
logger.info(f'✅ Уменьшен счетчик пользователей для серверов: {server_squad_ids}')
|
||||
logger.info('✅ Уменьшен счетчик пользователей для серверов', server_squad_ids=server_squad_ids)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка уменьшения счетчика пользователей: {e}')
|
||||
logger.error('Ошибка уменьшения счетчика пользователей', error=e)
|
||||
raise
|
||||
|
||||
|
||||
@@ -836,12 +830,12 @@ async def update_server_user_counts(
|
||||
|
||||
await db.flush()
|
||||
if add_set:
|
||||
logger.info('✅ Увеличен счетчик пользователей для серверов: %s', sorted(add_set))
|
||||
logger.info('✅ Увеличен счетчик пользователей для серверов', sorted=sorted(add_set))
|
||||
if remove_set:
|
||||
logger.info('✅ Уменьшен счетчик пользователей для серверов: %s', sorted(remove_set))
|
||||
logger.info('✅ Уменьшен счетчик пользователей для серверов', sorted=sorted(remove_set))
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Ошибка обновления счетчиков серверов: %s', e)
|
||||
logger.error('Ошибка обновления счетчиков серверов', e=e)
|
||||
raise
|
||||
|
||||
|
||||
@@ -875,7 +869,7 @@ async def ensure_servers_synced(db: AsyncSession) -> None:
|
||||
server_count = result.scalar() or 0
|
||||
|
||||
if server_count > 0:
|
||||
logger.info(f'✅ В базе уже есть {server_count} серверов, пропускаем синхронизацию')
|
||||
logger.info('✅ В базе уже есть серверов, пропускаем синхронизацию', server_count=server_count)
|
||||
return
|
||||
|
||||
logger.info('🔄 Серверов в БД нет, начинаем синхронизацию с RemnaWave...')
|
||||
@@ -900,10 +894,10 @@ async def ensure_servers_synced(db: AsyncSession) -> None:
|
||||
|
||||
# Синхронизируем
|
||||
created, updated, removed = await sync_with_remnawave(db, squads)
|
||||
logger.info(f'✅ Серверы синхронизированы: +{created} ~{updated} -{removed}')
|
||||
logger.info('✅ Серверы синхронизированы: + ~', created=created, updated=updated, removed=removed)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Ошибка синхронизации серверов: {e}')
|
||||
logger.error('❌ Ошибка синхронизации серверов', error=e)
|
||||
|
||||
|
||||
async def sync_server_user_counts(db: AsyncSession) -> int:
|
||||
@@ -911,7 +905,7 @@ async def sync_server_user_counts(db: AsyncSession) -> int:
|
||||
all_servers_result = await db.execute(select(ServerSquad.id, ServerSquad.squad_uuid))
|
||||
all_servers = all_servers_result.fetchall()
|
||||
|
||||
logger.info(f'🔍 Найдено серверов для синхронизации: {len(all_servers)}')
|
||||
logger.info('🔍 Найдено серверов для синхронизации', all_servers_count=len(all_servers))
|
||||
|
||||
updated_count = 0
|
||||
for server_id, squad_uuid in all_servers:
|
||||
@@ -926,16 +920,18 @@ async def sync_server_user_counts(db: AsyncSession) -> int:
|
||||
)
|
||||
actual_users = count_result.scalar() or 0
|
||||
|
||||
logger.info(f'📊 Сервер {server_id} ({squad_uuid[:8]}): {actual_users} пользователей')
|
||||
logger.info(
|
||||
'📊 Сервер пользователей', 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))
|
||||
updated_count += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f'✅ Синхронизированы счетчики для {updated_count} серверов')
|
||||
logger.info('✅ Синхронизированы счетчики для серверов', updated_count=updated_count)
|
||||
return updated_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка синхронизации счетчиков пользователей: {e}')
|
||||
logger.error('Ошибка синхронизации счетчиков пользователей', error=e)
|
||||
await db.rollback()
|
||||
return 0
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import Squad
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def get_squad_by_uuid(db: AsyncSession, uuid: str) -> Squad | None:
|
||||
@@ -28,7 +27,7 @@ async def create_squad(
|
||||
await db.commit()
|
||||
await db.refresh(squad)
|
||||
|
||||
logger.info(f'✅ Создан сквад: {name}')
|
||||
logger.info('✅ Создан сквад', name=name)
|
||||
return squad
|
||||
|
||||
|
||||
|
||||
+205
-164
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
_WEBHOOK_GUARD_SECONDS = 60
|
||||
|
||||
@@ -50,7 +50,10 @@ async def get_subscription_by_user_id(db: AsyncSession, user_id: int) -> Subscri
|
||||
|
||||
if subscription:
|
||||
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)
|
||||
|
||||
@@ -93,16 +96,10 @@ async def create_trial_subscription(
|
||||
if random_squad:
|
||||
final_squads = [random_squad]
|
||||
logger.debug(
|
||||
'Выбран сквад %s для триальной подписки пользователя %s',
|
||||
random_squad,
|
||||
user_id,
|
||||
'Выбран сквад для триальной подписки пользователя', random_squad=random_squad, user_id=user_id
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'Не удалось получить сквад для триальной подписки пользователя %s: %s',
|
||||
user_id,
|
||||
error,
|
||||
)
|
||||
logger.error('Не удалось получить сквад для триальной подписки пользователя', user_id=user_id, error=error)
|
||||
|
||||
end_date = datetime.utcnow() + timedelta(days=duration_days)
|
||||
|
||||
@@ -119,9 +116,7 @@ async def create_trial_subscription(
|
||||
await db.commit()
|
||||
await db.refresh(existing)
|
||||
logger.info(
|
||||
'🎁 Обновлена PENDING триальная подписка %s для пользователя %s',
|
||||
existing.id,
|
||||
user_id,
|
||||
'🎁 Обновлена PENDING триальная подписка для пользователя', existing_id=existing.id, user_id=user_id
|
||||
)
|
||||
return existing
|
||||
|
||||
@@ -157,20 +152,14 @@ async def create_trial_subscription(
|
||||
server_ids = await get_server_ids_by_uuids(db, final_squads)
|
||||
if server_ids:
|
||||
await add_user_to_servers(db, server_ids)
|
||||
logger.info(
|
||||
'📈 Обновлен счетчик пользователей для триальных сквадов %s',
|
||||
final_squads,
|
||||
)
|
||||
logger.info('📈 Обновлен счетчик пользователей для триальных сквадов', final_squads=final_squads)
|
||||
else:
|
||||
logger.warning(
|
||||
'⚠️ Не удалось найти серверы для обновления счетчика (сквады %s)',
|
||||
final_squads,
|
||||
)
|
||||
logger.warning('⚠️ Не удалось найти серверы для обновления счетчика (сквады)', final_squads=final_squads)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'⚠️ Ошибка обновления счетчика пользователей для триальных сквадов %s: %s',
|
||||
final_squads,
|
||||
error,
|
||||
'⚠️ Ошибка обновления счетчика пользователей для триальных сквадов',
|
||||
final_squads=final_squads,
|
||||
error=error,
|
||||
)
|
||||
|
||||
return subscription
|
||||
@@ -211,7 +200,10 @@ async def create_paid_subscription(
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(
|
||||
f'💎 Создана платная подписка для пользователя {user_id}, ID: {subscription.id}, статус: {subscription.status}'
|
||||
'💎 Создана платная подписка для пользователя ID: статус',
|
||||
user_id=user_id,
|
||||
subscription_id=subscription.id,
|
||||
status=subscription.status,
|
||||
)
|
||||
|
||||
squad_uuids = list(connected_squads or [])
|
||||
@@ -226,21 +218,21 @@ async def create_paid_subscription(
|
||||
if server_ids:
|
||||
await add_user_to_servers(db, server_ids)
|
||||
logger.info(
|
||||
'📈 Обновлен счетчик пользователей для платной подписки пользователя %s (сквады: %s)',
|
||||
user_id,
|
||||
squad_uuids,
|
||||
'📈 Обновлен счетчик пользователей для платной подписки пользователя (сквады:)',
|
||||
user_id=user_id,
|
||||
squad_uuids=squad_uuids,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
'⚠️ Не удалось найти серверы для обновления счетчика платной подписки пользователя %s (сквады: %s)',
|
||||
user_id,
|
||||
squad_uuids,
|
||||
'⚠️ Не удалось найти серверы для обновления счетчика платной подписки пользователя (сквады:)',
|
||||
user_id=user_id,
|
||||
squad_uuids=squad_uuids,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'⚠️ Ошибка обновления счетчика пользователей серверов для платной подписки пользователя %s: %s',
|
||||
user_id,
|
||||
error,
|
||||
'⚠️ Ошибка обновления счетчика пользователей серверов для платной подписки пользователя',
|
||||
user_id=user_id,
|
||||
error=error,
|
||||
)
|
||||
|
||||
return subscription
|
||||
@@ -313,16 +305,16 @@ async def replace_subscription(
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'♻️ Обновлены параметры подписки %s: удалено сквадов %s, добавлено %s',
|
||||
subscription.id,
|
||||
len(squads_to_remove),
|
||||
len(squads_to_add),
|
||||
'♻️ Обновлены параметры подписки : удалено сквадов , добавлено',
|
||||
subscription_id=subscription.id,
|
||||
squads_to_remove_count=len(squads_to_remove),
|
||||
squads_to_add_count=len(squads_to_add),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'⚠️ Ошибка обновления счетчиков серверов при замене подписки %s: %s',
|
||||
subscription.id,
|
||||
error,
|
||||
'⚠️ Ошибка обновления счетчиков серверов при замене подписки',
|
||||
subscription_id=subscription.id,
|
||||
error=error,
|
||||
)
|
||||
|
||||
return subscription
|
||||
@@ -351,9 +343,12 @@ async def extend_subscription(
|
||||
"""
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
logger.info(f'🔄 Продление подписки {subscription.id} на {days} дней')
|
||||
logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days)
|
||||
logger.info(
|
||||
f'📊 Текущие параметры: статус={subscription.status}, окончание={subscription.end_date}, тариф={subscription.tariff_id}'
|
||||
'📊 Текущие параметры: статус=, окончание=, тариф',
|
||||
status=subscription.status,
|
||||
end_date=subscription.end_date,
|
||||
tariff_id=subscription.tariff_id,
|
||||
)
|
||||
|
||||
# Определяем, происходит ли СМЕНА тарифа (а не продление того же)
|
||||
@@ -361,7 +356,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)
|
||||
|
||||
if is_tariff_change:
|
||||
logger.info(f'🔄 Обнаружена СМЕНА тарифа: {subscription.tariff_id} → {tariff_id}')
|
||||
logger.info('🔄 Обнаружена СМЕНА тарифа: →', tariff_id=subscription.tariff_id, tariff_id_2=tariff_id)
|
||||
|
||||
# Бонусные дни от триала - добавляются ТОЛЬКО когда подписка истекла
|
||||
# и мы начинаем отсчёт с текущей даты. НЕ начисляются при смене тарифа.
|
||||
@@ -371,9 +366,7 @@ async def extend_subscription(
|
||||
if days < 0:
|
||||
subscription.end_date = subscription.end_date + timedelta(days=days)
|
||||
logger.info(
|
||||
'📅 Срок подписки уменьшен на %s дней, новая дата окончания: %s',
|
||||
abs(days),
|
||||
subscription.end_date,
|
||||
'📅 Срок подписки уменьшен на дней, новая дата окончания', abs=abs(days), end_date=subscription.end_date
|
||||
)
|
||||
elif is_tariff_change:
|
||||
# При СМЕНЕ тарифа срок начинается с текущей даты + бонус от триала
|
||||
@@ -383,19 +376,19 @@ async def extend_subscription(
|
||||
if remaining.total_seconds() > 0:
|
||||
bonus_days = max(0, remaining.days)
|
||||
logger.info(
|
||||
'🎁 Обнаружен остаток триала: %s дней для подписки %s',
|
||||
bonus_days,
|
||||
subscription.id,
|
||||
'🎁 Обнаружен остаток триала: дней для подписки',
|
||||
bonus_days=bonus_days,
|
||||
subscription_id=subscription.id,
|
||||
)
|
||||
total_days = days + bonus_days
|
||||
subscription.end_date = current_time + timedelta(days=total_days)
|
||||
subscription.start_date = current_time
|
||||
logger.info(f'📅 СМЕНА тарифа: срок начинается с текущей даты + {total_days} дней')
|
||||
logger.info('📅 СМЕНА тарифа: срок начинается с текущей даты + дней', total_days=total_days)
|
||||
elif subscription.end_date > current_time:
|
||||
# Подписка активна - просто добавляем дни к текущей дате окончания
|
||||
# БЕЗ бонусных дней (они уже учтены в end_date)
|
||||
subscription.end_date = subscription.end_date + timedelta(days=days)
|
||||
logger.info(f'📅 Подписка активна, добавляем {days} дней к текущей дате окончания')
|
||||
logger.info('📅 Подписка активна, добавляем дней к текущей дате окончания', days=days)
|
||||
else:
|
||||
# Подписка истекла - начинаем с текущей даты + бонус от триала
|
||||
if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID:
|
||||
@@ -403,14 +396,19 @@ async def extend_subscription(
|
||||
pass
|
||||
total_days = days + bonus_days
|
||||
subscription.end_date = current_time + timedelta(days=total_days)
|
||||
logger.info(f'📅 Подписка истекла, устанавливаем новую дату окончания на {total_days} дней')
|
||||
logger.info('📅 Подписка истекла, устанавливаем новую дату окончания на дней', total_days=total_days)
|
||||
|
||||
# УДАЛЕНО: Автоматическая конвертация триала по длительности
|
||||
# Теперь триал конвертируется ТОЛЬКО после успешного коммита продления
|
||||
# и ТОЛЬКО вызывающей функцией (например, _auto_extend_subscription)
|
||||
|
||||
# Логируем статус подписки перед проверкой
|
||||
logger.info(f'🔄 Продление подписки {subscription.id}, текущий статус: {subscription.status}, дни: {days}')
|
||||
logger.info(
|
||||
'🔄 Продление подписки текущий статус: дни',
|
||||
subscription_id=subscription.id,
|
||||
status=subscription.status,
|
||||
days=days,
|
||||
)
|
||||
|
||||
if days > 0 and subscription.status in (
|
||||
SubscriptionStatus.EXPIRED.value,
|
||||
@@ -419,23 +417,21 @@ async def extend_subscription(
|
||||
previous_status = subscription.status
|
||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||
logger.info(
|
||||
'🔄 Статус подписки %s изменён с %s на ACTIVE',
|
||||
subscription.id,
|
||||
previous_status,
|
||||
'🔄 Статус подписки изменён с на ACTIVE', subscription_id=subscription.id, previous_status=previous_status
|
||||
)
|
||||
elif days > 0 and subscription.status == SubscriptionStatus.PENDING.value:
|
||||
logger.warning('⚠️ Попытка продлить PENDING подписку %s, дни: %s', subscription.id, days)
|
||||
logger.warning('⚠️ Попытка продлить PENDING подписку , дни', subscription_id=subscription.id, days=days)
|
||||
|
||||
# Обновляем параметры тарифа, если переданы
|
||||
if tariff_id is not None:
|
||||
old_tariff_id = subscription.tariff_id
|
||||
subscription.tariff_id = tariff_id
|
||||
logger.info(f'📦 Обновлен тариф подписки: {old_tariff_id} → {tariff_id}')
|
||||
logger.info('📦 Обновлен тариф подписки: →', old_tariff_id=old_tariff_id, tariff_id=tariff_id)
|
||||
|
||||
# При покупке тарифа сбрасываем триальный статус
|
||||
if subscription.is_trial:
|
||||
subscription.is_trial = False
|
||||
logger.info(f'🎓 Подписка {subscription.id} конвертирована из триала в платную')
|
||||
logger.info('🎓 Подписка конвертирована из триала в платную', subscription_id=subscription.id)
|
||||
|
||||
if traffic_limit_gb is not None:
|
||||
old_traffic = subscription.traffic_limit_gb
|
||||
@@ -452,14 +448,19 @@ async def extend_subscription(
|
||||
subscription.purchased_traffic_gb = 0
|
||||
subscription.traffic_reset_at = None
|
||||
logger.info(
|
||||
f'📊 Обновлен лимит трафика: {old_traffic} ГБ → {traffic_limit_gb} ГБ (смена тарифа, докупки сброшены)'
|
||||
'📊 Обновлен лимит трафика: ГБ → ГБ (смена тарифа, докупки сброшены)',
|
||||
old_traffic=old_traffic,
|
||||
traffic_limit_gb=traffic_limit_gb,
|
||||
)
|
||||
else:
|
||||
# При ПРОДЛЕНИИ того же тарифа — сохраняем докупленный трафик
|
||||
purchased = subscription.purchased_traffic_gb or 0
|
||||
subscription.traffic_limit_gb = traffic_limit_gb + purchased
|
||||
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:
|
||||
subscription.traffic_used_gb = 0.0
|
||||
@@ -475,12 +476,12 @@ async def extend_subscription(
|
||||
if device_limit is not None:
|
||||
old_devices = subscription.device_limit
|
||||
subscription.device_limit = device_limit
|
||||
logger.info(f'📱 Обновлен лимит устройств: {old_devices} → {device_limit}')
|
||||
logger.info('📱 Обновлен лимит устройств: →', old_devices=old_devices, device_limit=device_limit)
|
||||
|
||||
if connected_squads is not None:
|
||||
old_squads = subscription.connected_squads
|
||||
subscription.connected_squads = connected_squads
|
||||
logger.info(f'🌍 Обновлены сквады: {old_squads} → {connected_squads}')
|
||||
logger.info('🌍 Обновлены сквады: →', old_squads=old_squads, connected_squads=connected_squads)
|
||||
|
||||
# Обработка daily полей при смене тарифа
|
||||
if is_tariff_change and tariff_id is not None:
|
||||
@@ -514,7 +515,11 @@ async def extend_subscription(
|
||||
subscription.traffic_limit_gb = fixed_limit
|
||||
subscription.purchased_traffic_gb = 0
|
||||
subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика
|
||||
logger.info(f'🔄 Сброс трафика при продлении (fixed_with_topup): {old_limit} ГБ → {fixed_limit} ГБ')
|
||||
logger.info(
|
||||
'🔄 Сброс трафика при продлении (fixed_with_topup): ГБ → ГБ',
|
||||
old_limit=old_limit,
|
||||
fixed_limit=fixed_limit,
|
||||
)
|
||||
|
||||
subscription.updated_at = current_time
|
||||
|
||||
@@ -522,8 +527,8 @@ async def extend_subscription(
|
||||
await db.refresh(subscription)
|
||||
await clear_notifications(db, subscription.id)
|
||||
|
||||
logger.info(f'✅ Подписка продлена до: {subscription.end_date}')
|
||||
logger.info(f'📊 Новые параметры: статус={subscription.status}, окончание={subscription.end_date}')
|
||||
logger.info('✅ Подписка продлена до', end_date=subscription.end_date)
|
||||
logger.info('📊 Новые параметры: статус=, окончание', status=subscription.status, end_date=subscription.end_date)
|
||||
|
||||
return subscription
|
||||
|
||||
@@ -570,7 +575,10 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription,
|
||||
await db.refresh(subscription)
|
||||
|
||||
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
|
||||
|
||||
@@ -582,7 +590,7 @@ async def add_subscription_devices(db: AsyncSession, subscription: Subscription,
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'📱 К подписке пользователя {subscription.user_id} добавлено {devices} устройств')
|
||||
logger.info('📱 К подписке пользователя добавлено устройств', user_id=subscription.user_id, devices=devices)
|
||||
return subscription
|
||||
|
||||
|
||||
@@ -594,7 +602,7 @@ async def add_subscription_squad(db: AsyncSession, subscription: Subscription, s
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'🌍 К подписке пользователя {subscription.user_id} добавлен сквад {squad_uuid}')
|
||||
logger.info('🌍 К подписке пользователя добавлен сквад', user_id=subscription.user_id, squad_uuid=squad_uuid)
|
||||
|
||||
return subscription
|
||||
|
||||
@@ -609,7 +617,7 @@ async def remove_subscription_squad(db: AsyncSession, subscription: Subscription
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'🚫 Из подписки пользователя {subscription.user_id} удален сквад {squad_uuid}')
|
||||
logger.info('🚫 Из подписки пользователя удален сквад', user_id=subscription.user_id, squad_uuid=squad_uuid)
|
||||
|
||||
return subscription
|
||||
|
||||
@@ -639,11 +647,7 @@ async def decrement_subscription_server_counts(
|
||||
ids_from_links = await get_subscription_server_ids(db, sub_id)
|
||||
server_ids.update(ids_from_links)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'⚠️ Не удалось получить серверы подписки %s для уменьшения счетчика: %s',
|
||||
sub_id,
|
||||
error,
|
||||
)
|
||||
logger.error('⚠️ Не удалось получить серверы подписки для уменьшения счетчика', sub_id=sub_id, error=error)
|
||||
|
||||
connected_squads = list(subscription.connected_squads or [])
|
||||
if connected_squads:
|
||||
@@ -653,11 +657,7 @@ async def decrement_subscription_server_counts(
|
||||
squad_server_ids = await get_server_ids_by_uuids(db, connected_squads)
|
||||
server_ids.update(squad_server_ids)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'⚠️ Не удалось сопоставить сквады подписки %s с серверами: %s',
|
||||
sub_id,
|
||||
error,
|
||||
)
|
||||
logger.error('⚠️ Не удалось сопоставить сквады подписки с серверами', sub_id=sub_id, error=error)
|
||||
|
||||
if not server_ids:
|
||||
return
|
||||
@@ -670,16 +670,16 @@ async def decrement_subscription_server_counts(
|
||||
await remove_user_from_servers(db, list(server_ids))
|
||||
except StaleDataError:
|
||||
logger.warning(
|
||||
'⚠️ Подписка %s уже удалена (StaleDataError), пропускаем декремент серверов %s',
|
||||
sub_id,
|
||||
list(server_ids),
|
||||
'⚠️ Подписка уже удалена (StaleDataError), пропускаем декремент серверов',
|
||||
sub_id=sub_id,
|
||||
list=list(server_ids),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'⚠️ Ошибка уменьшения счетчика пользователей серверов %s для подписки %s: %s',
|
||||
list(server_ids),
|
||||
sub_id,
|
||||
error,
|
||||
'⚠️ Ошибка уменьшения счетчика пользователей серверов для подписки',
|
||||
list=list(server_ids),
|
||||
sub_id=sub_id,
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
@@ -694,7 +694,7 @@ async def update_subscription_autopay(
|
||||
await db.refresh(subscription)
|
||||
|
||||
status = 'включен' if enabled else 'выключен'
|
||||
logger.info(f'💳 Автоплатеж для подписки пользователя {subscription.user_id} {status}')
|
||||
logger.info('💳 Автоплатеж для подписки пользователя', user_id=subscription.user_id, status=status)
|
||||
return subscription
|
||||
|
||||
|
||||
@@ -705,7 +705,7 @@ async def deactivate_subscription(db: AsyncSession, subscription: Subscription)
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'❌ Подписка пользователя {subscription.user_id} деактивирована')
|
||||
logger.info('❌ Подписка пользователя деактивирована', user_id=subscription.user_id)
|
||||
return subscription
|
||||
|
||||
|
||||
@@ -844,9 +844,9 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
|
||||
renewals_count = conversion_stats.get('month_conversions', 0)
|
||||
|
||||
logger.info('📊 Статистика конверсии из таблицы conversions:')
|
||||
logger.info(f' Общее количество конверсий: {conversion_stats.get("total_conversions", 0)}')
|
||||
logger.info(f' Процент конверсии: {trial_to_paid_conversion}%')
|
||||
logger.info(f' Конверсий за месяц: {renewals_count}')
|
||||
logger.info('Общее количество конверсий', get=conversion_stats.get('total_conversions', 0))
|
||||
logger.info('Процент конверсии', trial_to_paid_conversion=trial_to_paid_conversion)
|
||||
logger.info('Конверсий за месяц', renewals_count=renewals_count)
|
||||
|
||||
except ImportError:
|
||||
logger.warning('⚠️ Таблица subscription_conversions не найдена, используем старую логику')
|
||||
@@ -943,9 +943,7 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
|
||||
)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error(
|
||||
'Не удалось обновить счётчики серверов при сбросе триала %s: %s',
|
||||
subscription.id,
|
||||
error,
|
||||
'Не удалось обновить счётчики серверов при сбросе триала', subscription_id=subscription.id, error=error
|
||||
)
|
||||
|
||||
subscription_ids = [subscription.id for subscription in subscriptions]
|
||||
@@ -954,11 +952,7 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
|
||||
try:
|
||||
await db.execute(delete(SubscriptionServer).where(SubscriptionServer.subscription_id.in_(subscription_ids)))
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error(
|
||||
'Ошибка удаления серверных связей триалов %s: %s',
|
||||
subscription_ids,
|
||||
error,
|
||||
)
|
||||
logger.error('Ошибка удаления серверных связей триалов', subscription_ids=subscription_ids, error=error)
|
||||
raise
|
||||
|
||||
await db.execute(delete(Subscription).where(Subscription.id.in_(subscription_ids)))
|
||||
@@ -967,10 +961,10 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
|
||||
await db.commit()
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
await db.rollback()
|
||||
logger.error('Ошибка сохранения сброса триалов: %s', error)
|
||||
logger.error('Ошибка сохранения сброса триалов', error=error)
|
||||
raise
|
||||
|
||||
logger.info('♻️ Сброшено триальных подписок: %s', reset_count)
|
||||
logger.info('♻️ Сброшено триальных подписок', reset_count=reset_count)
|
||||
return reset_count
|
||||
|
||||
|
||||
@@ -1047,7 +1041,12 @@ async def add_subscription_servers(
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'🌐 К подписке {subscription.id} добавлено {len(server_squad_ids)} серверов с ценами: {paid_prices}')
|
||||
logger.info(
|
||||
'🌐 К подписке добавлено серверов с ценами',
|
||||
subscription_id=subscription.id,
|
||||
server_squad_ids_count=len(server_squad_ids),
|
||||
paid_prices=paid_prices,
|
||||
)
|
||||
return subscription
|
||||
|
||||
|
||||
@@ -1084,7 +1083,7 @@ async def get_servers_monthly_prices(
|
||||
user_promo_group = user.get_primary_promo_group()
|
||||
user_promo_group_id = user_promo_group.id if user_promo_group else None
|
||||
except Exception as e:
|
||||
logger.warning(f'Не удалось получить промогруппу пользователя: {e}')
|
||||
logger.warning('Не удалось получить промогруппу пользователя', error=e)
|
||||
|
||||
for server_id in server_squad_ids:
|
||||
# Загружаем сервер с промогруппами
|
||||
@@ -1110,9 +1109,11 @@ async def get_servers_monthly_prices(
|
||||
else:
|
||||
# Сервер недоступен для промогруппы пользователя
|
||||
logger.warning(
|
||||
f'⚠️ Сервер {server.display_name} (id={server_id}) недоступен для '
|
||||
f'промогруппы пользователя (promo_group_id={user_promo_group_id}), '
|
||||
f'allowed_promo_groups={[pg.id for pg in server.allowed_promo_groups] if server.allowed_promo_groups else []}'
|
||||
'⚠️ Сервер (id=) недоступен для промогруппы пользователя (promo_group_id=), allowed_promo_groups',
|
||||
display_name=server.display_name,
|
||||
server_id=server_id,
|
||||
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) # Всё равно берём реальную цену
|
||||
|
||||
@@ -1227,8 +1228,10 @@ async def calculate_subscription_total_cost(
|
||||
],
|
||||
}
|
||||
|
||||
logger.debug(f'📊 Расчет стоимости подписки на {period_days} дней ({months_in_period} мес):')
|
||||
logger.debug(f' Базовый период: {base_price / 100}₽')
|
||||
logger.debug(
|
||||
'📊 Расчет стоимости подписки на дней ( мес)', period_days=period_days, months_in_period=months_in_period
|
||||
)
|
||||
logger.debug('Базовый период: ₽', base_price=base_price / 100)
|
||||
if total_traffic_price > 0:
|
||||
message = f' Трафик: {traffic_price_per_month / 100}₽/мес × {months_in_period} = {total_traffic_price / 100}₽'
|
||||
if total_traffic_discount > 0:
|
||||
@@ -1248,7 +1251,7 @@ async def calculate_subscription_total_cost(
|
||||
if total_devices_discount > 0:
|
||||
message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)'
|
||||
logger.debug(message)
|
||||
logger.debug(f' ИТОГО: {total_cost / 100}₽')
|
||||
logger.debug('ИТОГО: ₽', total_cost=total_cost / 100)
|
||||
|
||||
return total_cost, details
|
||||
|
||||
@@ -1300,11 +1303,11 @@ async def remove_subscription_servers(db: AsyncSession, subscription_id: int, se
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
logger.info(f'🗑️ Удалены серверы {server_squad_ids} из подписки {subscription_id}')
|
||||
logger.info('🗑️ Удалены серверы из подписки', server_squad_ids=server_squad_ids, subscription_id=subscription_id)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка удаления серверов из подписки: {e}')
|
||||
logger.error('Ошибка удаления серверов из подписки', error=e)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -1395,8 +1398,13 @@ async def get_subscription_renewal_cost(
|
||||
|
||||
total_cost = base_price + total_servers_cost + total_traffic_cost + total_devices_cost
|
||||
|
||||
logger.info(f'💰 Расчет продления подписки {subscription_id} на {period_days} дней ({months_in_period} мес):')
|
||||
logger.info(f' 📅 Период: {base_price / 100}₽')
|
||||
logger.info(
|
||||
'💰 Расчет продления подписки на дней ( мес)',
|
||||
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:
|
||||
message = f' 🌍 Серверы: {servers_price_per_month / 100}₽/мес × {months_in_period} = {total_servers_cost / 100}₽'
|
||||
if total_servers_discount > 0:
|
||||
@@ -1414,12 +1422,12 @@ async def get_subscription_renewal_cost(
|
||||
if total_devices_discount > 0:
|
||||
message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)'
|
||||
logger.info(message)
|
||||
logger.info(f' 💎 ИТОГО: {total_cost / 100}₽')
|
||||
logger.info('💎 ИТОГО: ₽', total_cost=total_cost / 100)
|
||||
|
||||
return total_cost
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка расчета стоимости продления: {e}')
|
||||
logger.error('Ошибка расчета стоимости продления', error=e)
|
||||
from app.config import PERIOD_PRICES
|
||||
|
||||
return PERIOD_PRICES.get(period_days, 0)
|
||||
@@ -1508,7 +1516,7 @@ async def calculate_addon_cost_for_remaining_period(
|
||||
)
|
||||
logger.info(message)
|
||||
|
||||
logger.info(f'💰 Итого доплата за {months_to_pay} мес: {total_cost / 100}₽')
|
||||
logger.info('💰 Итого доплата за мес: ₽', months_to_pay=months_to_pay, total_cost=total_cost / 100)
|
||||
return total_cost
|
||||
|
||||
|
||||
@@ -1519,7 +1527,7 @@ async def expire_subscription(db: AsyncSession, subscription: Subscription) -> S
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'⏰ Подписка пользователя {subscription.user_id} помечена как истёкшая')
|
||||
logger.info('⏰ Подписка пользователя помечена как истёкшая', user_id=subscription.user_id)
|
||||
return subscription
|
||||
|
||||
|
||||
@@ -1527,28 +1535,30 @@ async def check_and_update_subscription_status(db: AsyncSession, subscription: S
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
logger.info(
|
||||
'🔍 Проверка статуса подписки %s, текущий статус: %s, дата окончания: %s, текущее время: %s',
|
||||
subscription.id,
|
||||
subscription.status,
|
||||
format_local_datetime(subscription.end_date),
|
||||
format_local_datetime(current_time),
|
||||
'🔍 Проверка статуса подписки , текущий статус дата окончания текущее время',
|
||||
subscription_id=subscription.id,
|
||||
subscription_status=subscription.status,
|
||||
format_local_datetime=format_local_datetime(subscription.end_date),
|
||||
format_local_datetime_2=format_local_datetime(current_time),
|
||||
)
|
||||
|
||||
# Для суточных тарифов с паузой не меняем статус на expired
|
||||
# (время "заморожено" пока пользователь на паузе)
|
||||
is_daily_paused = getattr(subscription, 'is_daily_paused', False)
|
||||
if is_daily_paused:
|
||||
logger.info(f'⏸️ Суточная подписка {subscription.id} на паузе, пропускаем проверку истечения')
|
||||
logger.info('⏸️ Суточная подписка на паузе, пропускаем проверку истечения', subscription_id=subscription.id)
|
||||
return subscription
|
||||
|
||||
if subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date <= current_time:
|
||||
# Детальное логирование для отладки проблемы с деактивацией
|
||||
time_diff = current_time - subscription.end_date
|
||||
logger.warning(
|
||||
f'⏰ DEACTIVATION: подписка {subscription.id} (user_id={subscription.user_id}) '
|
||||
f'деактивируется в check_and_update_subscription_status. '
|
||||
f'end_date={subscription.end_date}, current_time={current_time}, '
|
||||
f'просрочена на {time_diff}'
|
||||
'⏰ DEACTIVATION: подписка (user_id=) деактивируется в check_and_update_subscription_status. end_date=, current_time=, просрочена на',
|
||||
subscription_id=subscription.id,
|
||||
user_id=subscription.user_id,
|
||||
end_date=subscription.end_date,
|
||||
current_time=current_time,
|
||||
time_diff=time_diff,
|
||||
)
|
||||
|
||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||
@@ -1557,9 +1567,9 @@ async def check_and_update_subscription_status(db: AsyncSession, subscription: S
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f"⏰ Статус подписки пользователя {subscription.user_id} изменен на 'expired'")
|
||||
logger.info("⏰ Статус подписки пользователя изменен на 'expired'", user_id=subscription.user_id)
|
||||
elif subscription.status == SubscriptionStatus.PENDING.value:
|
||||
logger.info(f'ℹ️ Проверка PENDING подписки {subscription.id}, статус остается без изменений')
|
||||
logger.info('ℹ️ Проверка PENDING подписки статус остается без изменений', subscription_id=subscription.id)
|
||||
|
||||
return subscription
|
||||
|
||||
@@ -1614,7 +1624,7 @@ async def create_subscription_no_commit(
|
||||
await db.flush()
|
||||
|
||||
# Не коммитим сразу, оставляем для пакетной обработки
|
||||
logger.info(f'✅ Подготовлена подписка для пользователя {user_id} (ожидает коммита)')
|
||||
logger.info('✅ Подготовлена подписка для пользователя (ожидает коммита)', user_id=user_id)
|
||||
return subscription
|
||||
|
||||
|
||||
@@ -1662,7 +1672,7 @@ async def create_subscription(
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'✅ Создана подписка для пользователя {user_id}')
|
||||
logger.info('✅ Создана подписка для пользователя', user_id=user_id)
|
||||
return subscription
|
||||
|
||||
|
||||
@@ -1694,9 +1704,9 @@ async def create_pending_subscription(
|
||||
and existing_subscription.end_date > current_time
|
||||
):
|
||||
logger.warning(
|
||||
'⚠️ Попытка создать pending %sподписку для активного пользователя %s. Возвращаем существующую запись.',
|
||||
trial_label,
|
||||
user_id,
|
||||
'⚠️ Попытка создать pending подписку для активного пользователя . Возвращаем существующую запись.',
|
||||
trial_label=trial_label,
|
||||
user_id=user_id,
|
||||
)
|
||||
return existing_subscription
|
||||
|
||||
@@ -1714,11 +1724,11 @@ async def create_pending_subscription(
|
||||
await db.refresh(existing_subscription)
|
||||
|
||||
logger.info(
|
||||
'♻️ Обновлена ожидающая %sподписка пользователя %s, ID: %s, метод оплаты: %s',
|
||||
trial_label,
|
||||
user_id,
|
||||
existing_subscription.id,
|
||||
payment_method,
|
||||
'♻️ Обновлена ожидающая подписка пользователя , ID метод оплаты',
|
||||
trial_label=trial_label,
|
||||
user_id=user_id,
|
||||
existing_subscription_id=existing_subscription.id,
|
||||
payment_method=payment_method,
|
||||
)
|
||||
return existing_subscription
|
||||
|
||||
@@ -1740,11 +1750,11 @@ async def create_pending_subscription(
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(
|
||||
'💳 Создана ожидающая %sподписка для пользователя %s, ID: %s, метод оплаты: %s',
|
||||
trial_label,
|
||||
user_id,
|
||||
subscription.id,
|
||||
payment_method,
|
||||
'💳 Создана ожидающая подписка для пользователя , ID метод оплаты',
|
||||
trial_label=trial_label,
|
||||
user_id=user_id,
|
||||
subscription_id=subscription.id,
|
||||
payment_method=payment_method,
|
||||
)
|
||||
|
||||
return subscription
|
||||
@@ -1777,7 +1787,7 @@ async def create_pending_trial_subscription(
|
||||
|
||||
async def activate_pending_subscription(db: AsyncSession, user_id: int, period_days: int = None) -> Subscription | None:
|
||||
"""Активирует pending подписку пользователя, меняя её статус на ACTIVE."""
|
||||
logger.info(f'Активация pending подписки: пользователь {user_id}, период {period_days} дней')
|
||||
logger.info('Активация pending подписки: пользователь период дней', user_id=user_id, period_days=period_days)
|
||||
|
||||
# Находим pending подписку пользователя
|
||||
result = await db.execute(
|
||||
@@ -1788,11 +1798,14 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
|
||||
pending_subscription = result.scalar_one_or_none()
|
||||
|
||||
if not pending_subscription:
|
||||
logger.warning(f'Не найдена pending подписка для пользователя {user_id}')
|
||||
logger.warning('Не найдена pending подписка для пользователя', user_id=user_id)
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
f'Найдена pending подписка {pending_subscription.id} для пользователя {user_id}, статус: {pending_subscription.status}'
|
||||
'Найдена pending подписка для пользователя статус',
|
||||
pending_subscription_id=pending_subscription.id,
|
||||
user_id=user_id,
|
||||
status=pending_subscription.status,
|
||||
)
|
||||
|
||||
# Обновляем статус подписки на ACTIVE
|
||||
@@ -1812,7 +1825,9 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
|
||||
await db.commit()
|
||||
await db.refresh(pending_subscription)
|
||||
|
||||
logger.info(f'Подписка пользователя {user_id} активирована, ID: {pending_subscription.id}')
|
||||
logger.info(
|
||||
'Подписка пользователя активирована, ID', user_id=user_id, pending_subscription_id=pending_subscription.id
|
||||
)
|
||||
|
||||
return pending_subscription
|
||||
|
||||
@@ -1823,7 +1838,11 @@ async def activate_pending_trial_subscription(
|
||||
user_id: int,
|
||||
) -> Subscription | None:
|
||||
"""Активирует pending триальную подписку по её ID после оплаты."""
|
||||
logger.info(f'Активация pending триальной подписки: subscription_id={subscription_id}, user_id={user_id}')
|
||||
logger.info(
|
||||
'Активация pending триальной подписки: subscription_id=, user_id',
|
||||
subscription_id=subscription_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Находим pending подписку по ID
|
||||
result = await db.execute(
|
||||
@@ -1839,10 +1858,16 @@ async def activate_pending_trial_subscription(
|
||||
pending_subscription = result.scalar_one_or_none()
|
||||
|
||||
if not pending_subscription:
|
||||
logger.warning(f'Не найдена pending триальная подписка {subscription_id} для пользователя {user_id}')
|
||||
logger.warning(
|
||||
'Не найдена pending триальная подписка для пользователя', subscription_id=subscription_id, user_id=user_id
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info(f'Найдена pending триальная подписка {pending_subscription.id}, статус: {pending_subscription.status}')
|
||||
logger.info(
|
||||
'Найдена pending триальная подписка статус',
|
||||
pending_subscription_id=pending_subscription.id,
|
||||
status=pending_subscription.status,
|
||||
)
|
||||
|
||||
# Обновляем статус подписки на ACTIVE
|
||||
current_time = datetime.utcnow()
|
||||
@@ -1865,7 +1890,11 @@ async def activate_pending_trial_subscription(
|
||||
await db.commit()
|
||||
await db.refresh(pending_subscription)
|
||||
|
||||
logger.info(f'Триальная подписка {pending_subscription.id} активирована для пользователя {user_id}')
|
||||
logger.info(
|
||||
'Триальная подписка активирована для пользователя',
|
||||
pending_subscription_id=pending_subscription.id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
return pending_subscription
|
||||
|
||||
@@ -1911,7 +1940,7 @@ async def get_daily_subscriptions_for_charge(db: AsyncSession) -> list[Subscript
|
||||
result = await db.execute(query)
|
||||
subscriptions = result.scalars().all()
|
||||
|
||||
logger.info(f'🔍 Найдено {len(subscriptions)} суточных подписок для списания')
|
||||
logger.info('🔍 Найдено суточных подписок для списания', subscriptions_count=len(subscriptions))
|
||||
|
||||
return list(subscriptions)
|
||||
|
||||
@@ -1948,7 +1977,7 @@ async def get_disabled_daily_subscriptions_for_resume(
|
||||
result = await db.execute(query)
|
||||
subscriptions = result.scalars().all()
|
||||
|
||||
logger.info(f'🔍 Найдено {len(subscriptions)} DISABLED суточных подписок для возобновления')
|
||||
logger.info('🔍 Найдено DISABLED суточных подписок для возобновления', subscriptions_count=len(subscriptions))
|
||||
|
||||
return list(subscriptions)
|
||||
|
||||
@@ -1959,14 +1988,18 @@ async def pause_daily_subscription(
|
||||
) -> Subscription:
|
||||
"""Приостанавливает суточную подписку (списание не будет происходить)."""
|
||||
if not subscription.is_daily_tariff:
|
||||
logger.warning(f'Попытка приостановить не-суточную подписку {subscription.id}')
|
||||
logger.warning('Попытка приостановить не-суточную подписку', subscription_id=subscription.id)
|
||||
return subscription
|
||||
|
||||
subscription.is_daily_paused = True
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'⏸️ Суточная подписка {subscription.id} приостановлена пользователем {subscription.user_id}')
|
||||
logger.info(
|
||||
'⏸️ Суточная подписка приостановлена пользователем',
|
||||
subscription_id=subscription.id,
|
||||
user_id=subscription.user_id,
|
||||
)
|
||||
|
||||
return subscription
|
||||
|
||||
@@ -1977,7 +2010,7 @@ async def resume_daily_subscription(
|
||||
) -> Subscription:
|
||||
"""Возобновляет суточную подписку (списание продолжится)."""
|
||||
if not subscription.is_daily_tariff:
|
||||
logger.warning(f'Попытка возобновить не-суточную подписку {subscription.id}')
|
||||
logger.warning('Попытка возобновить не-суточную подписку', subscription_id=subscription.id)
|
||||
return subscription
|
||||
|
||||
subscription.is_daily_paused = False
|
||||
@@ -1989,12 +2022,18 @@ async def resume_daily_subscription(
|
||||
# Обновляем время последнего списания для корректного расчёта следующего
|
||||
subscription.last_daily_charge_at = datetime.utcnow()
|
||||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
||||
logger.info(f'✅ Суточная подписка {subscription.id} восстановлена из {previous_status} в ACTIVE')
|
||||
logger.info(
|
||||
'✅ Суточная подписка восстановлена из в ACTIVE',
|
||||
subscription_id=subscription.id,
|
||||
previous_status=previous_status,
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(f'▶️ Суточная подписка {subscription.id} возобновлена пользователем {subscription.user_id}')
|
||||
logger.info(
|
||||
'▶️ Суточная подписка возобновлена пользователем', subscription_id=subscription.id, user_id=subscription.user_id
|
||||
)
|
||||
|
||||
return subscription
|
||||
|
||||
@@ -2012,7 +2051,7 @@ async def update_daily_charge_time(
|
||||
new_end_date = now + timedelta(days=1)
|
||||
if subscription.end_date is None or subscription.end_date < new_end_date:
|
||||
subscription.end_date = new_end_date
|
||||
logger.info(f'📅 Продлена подписка {subscription.id} до {new_end_date}')
|
||||
logger.info('📅 Продлена подписка до', subscription_id=subscription.id, new_end_date=new_end_date)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
@@ -2033,7 +2072,9 @@ async def suspend_daily_subscription_insufficient_balance(
|
||||
await db.refresh(subscription)
|
||||
|
||||
logger.info(
|
||||
f'⚠️ Суточная подписка {subscription.id} приостановлена: недостаточно средств (user_id={subscription.user_id})'
|
||||
'⚠️ Суточная подписка приостановлена: недостаточно средств (user_id=)',
|
||||
subscription_id=subscription.id,
|
||||
user_id=subscription.user_id,
|
||||
)
|
||||
|
||||
return subscription
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import SubscriptionConversion, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_subscription_conversion(
|
||||
@@ -32,7 +32,11 @@ async def create_subscription_conversion(
|
||||
await db.refresh(conversion)
|
||||
|
||||
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
|
||||
@@ -85,9 +89,9 @@ async def get_conversion_statistics(db: AsyncSession) -> dict:
|
||||
month_conversions = month_conversions_result.scalar() or 0
|
||||
|
||||
logger.info('📊 Статистика конверсий:')
|
||||
logger.info(f' Всего пользователей с подписками: {total_users_with_subscriptions}')
|
||||
logger.info(f' Оплативших подписку: {users_with_paid}')
|
||||
logger.info(f' Рассчитанная конверсия: {conversion_rate}%')
|
||||
logger.info('Всего пользователей с подписками', total_users_with_subscriptions=total_users_with_subscriptions)
|
||||
logger.info('Оплативших подписку', users_with_paid=users_with_paid)
|
||||
logger.info('Рассчитанная конверсия', conversion_rate=conversion_rate)
|
||||
|
||||
return {
|
||||
'total_conversions': total_conversions,
|
||||
|
||||
+22
-26
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -7,7 +6,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import PromoGroup, Subscription, Tariff
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def _normalize_period_prices(period_prices: dict[int, int] | None) -> dict[str, int]:
|
||||
@@ -241,13 +240,13 @@ async def create_tariff(
|
||||
await db.refresh(tariff)
|
||||
|
||||
logger.info(
|
||||
"Создан тариф '%s' (id=%s, tier=%s, traffic=%sGB, devices=%s, prices=%s)",
|
||||
tariff.name,
|
||||
tariff.id,
|
||||
tariff.tier_level,
|
||||
tariff.traffic_limit_gb,
|
||||
tariff.device_limit,
|
||||
normalized_prices,
|
||||
"Создан тариф '' (id tier traffic=GB, devices prices=)",
|
||||
tariff_name=tariff.name,
|
||||
tariff_id=tariff.id,
|
||||
tier_level=tariff.tier_level,
|
||||
traffic_limit_gb=tariff.traffic_limit_gb,
|
||||
device_limit=tariff.device_limit,
|
||||
normalized_prices=normalized_prices,
|
||||
)
|
||||
|
||||
return tariff
|
||||
@@ -365,11 +364,7 @@ async def update_tariff(
|
||||
await db.commit()
|
||||
await db.refresh(tariff)
|
||||
|
||||
logger.info(
|
||||
"Обновлен тариф '%s' (id=%s)",
|
||||
tariff.name,
|
||||
tariff.id,
|
||||
)
|
||||
logger.info("Обновлен тариф '' (id=)", tariff_name=tariff.name, tariff_id=tariff.id)
|
||||
|
||||
return tariff
|
||||
|
||||
@@ -393,10 +388,10 @@ async def delete_tariff(db: AsyncSession, tariff: Tariff) -> bool:
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"Удален тариф '%s' (id=%s), затронуто подписок: %s",
|
||||
tariff_name,
|
||||
tariff_id,
|
||||
affected_subscriptions,
|
||||
"Удален тариф '' (id=), затронуто подписок",
|
||||
tariff_name=tariff_name,
|
||||
tariff_id=tariff_id,
|
||||
affected_subscriptions=affected_subscriptions,
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -486,7 +481,7 @@ async def reorder_tariffs(
|
||||
for order, tariff_id in enumerate(tariff_order):
|
||||
await db.execute(update(Tariff).where(Tariff.id == tariff_id).values(display_order=order))
|
||||
|
||||
logger.info('Изменен порядок тарифов: %s', tariff_order)
|
||||
logger.info('Изменен порядок тарифов', tariff_order=tariff_order)
|
||||
|
||||
|
||||
async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
|
||||
@@ -522,7 +517,8 @@ async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
|
||||
# Тариф уже существует — НЕ перезаписываем настройки из конфига.
|
||||
# Админ управляет тарифом через кабинет, синхронизация не нужна.
|
||||
logger.info(
|
||||
"Дефолтный тариф 'Стандартный' (id=%s) уже существует, пропускаем sync из конфига", existing_tariff.id
|
||||
"Дефолтный тариф 'Стандартный' (id=) уже существует, пропускаем sync из конфига",
|
||||
existing_tariff_id=existing_tariff.id,
|
||||
)
|
||||
return existing_tariff
|
||||
|
||||
@@ -544,7 +540,7 @@ async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
|
||||
db.add(new_tariff)
|
||||
await db.commit()
|
||||
await db.refresh(new_tariff)
|
||||
logger.info("Создан дефолтный тариф 'Стандартный' из конфига: %s", period_prices)
|
||||
logger.info("Создан дефолтный тариф 'Стандартный' из конфига", period_prices=period_prices)
|
||||
return new_tariff
|
||||
|
||||
return None
|
||||
@@ -574,7 +570,7 @@ async def load_period_prices_from_db(db: AsyncSession) -> None:
|
||||
return
|
||||
|
||||
if not tariff.period_prices:
|
||||
logger.warning("Тариф '%s' (id=%s) найден, но period_prices пуст", tariff.name, tariff.id)
|
||||
logger.warning("Тариф '' (id=) найден, но period_prices пуст", tariff_name=tariff.name, tariff_id=tariff.id)
|
||||
return
|
||||
|
||||
# Преобразуем строковые ключи в int
|
||||
@@ -588,10 +584,10 @@ async def load_period_prices_from_db(db: AsyncSession) -> None:
|
||||
{f'{d}д': f'{p // 100}₽' for d, p in period_prices.items()},
|
||||
)
|
||||
else:
|
||||
logger.warning("Тариф '%s' не имеет активных периодов (все цены = 0)", tariff.name)
|
||||
logger.warning("Тариф '' не имеет активных периодов (все цены = 0)", tariff_name=tariff.name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Ошибка загрузки периодов из БД: %s', e)
|
||||
logger.error('Ошибка загрузки периодов из БД', e=e)
|
||||
|
||||
|
||||
async def ensure_tariffs_synced(db: AsyncSession) -> None:
|
||||
@@ -604,4 +600,4 @@ async def ensure_tariffs_synced(db: AsyncSession) -> None:
|
||||
# Загружаем периоды из БД в PERIOD_PRICES
|
||||
await load_period_prices_from_db(db)
|
||||
except Exception as e:
|
||||
logger.error('Ошибка синхронизации тарифов: %s', e)
|
||||
logger.error('Ошибка синхронизации тарифов', e=e)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, desc, func, or_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import SupportAuditLog, Ticket, TicketMessage, TicketStatus
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TicketCRUD:
|
||||
@@ -64,7 +64,7 @@ class TicketCRUD:
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning('Failed to emit ticket.created event: %s', error)
|
||||
logger.warning('Failed to emit ticket.created event', error=error)
|
||||
|
||||
return ticket
|
||||
|
||||
@@ -248,7 +248,7 @@ class TicketCRUD:
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning('Failed to emit ticket.status_changed event: %s', error)
|
||||
logger.warning('Failed to emit ticket.status_changed event', error=error)
|
||||
|
||||
return True
|
||||
|
||||
@@ -440,7 +440,7 @@ class TicketMessageCRUD:
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning('Failed to emit ticket.message_added event: %s', error)
|
||||
logger.warning('Failed to emit ticket.message_added event', error=error)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""CRUD operations for TicketNotification."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import desc, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -10,7 +10,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import Ticket, TicketNotification
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TicketNotificationCRUD:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import PaymentMethod, Transaction, TransactionType, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Реальные платёжные методы для подсчёта дохода
|
||||
# Исключены: MANUAL (админские), BALANCE (оплата с баланса), NULL (колесо, промокоды, бонусы)
|
||||
@@ -55,7 +55,12 @@ async def create_transaction(
|
||||
await db.commit()
|
||||
await db.refresh(transaction)
|
||||
|
||||
logger.info(f'💳 Создана транзакция: {type.value} на {amount_kopeks / 100}₽ для пользователя {user_id}')
|
||||
logger.info(
|
||||
'💳 Создана транзакция: на ₽ для пользователя',
|
||||
type_value=type.value,
|
||||
amount_kopeks=amount_kopeks / 100,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Отправляем событие о транзакции
|
||||
try:
|
||||
@@ -77,7 +82,7 @@ async def create_transaction(
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning('Failed to emit transaction event: %s', error)
|
||||
logger.warning('Failed to emit transaction event', error=error)
|
||||
|
||||
try:
|
||||
from app.services.promo_group_assignment import (
|
||||
@@ -86,11 +91,7 @@ async def create_transaction(
|
||||
|
||||
await maybe_assign_promo_group_by_total_spent(db, user_id)
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
'Не удалось проверить автовыдачу промогруппы для пользователя %s: %s',
|
||||
user_id,
|
||||
exc,
|
||||
)
|
||||
logger.debug('Не удалось проверить автовыдачу промогруппы для пользователя', user_id=user_id, exc=exc)
|
||||
if type == TransactionType.SUBSCRIPTION_PAYMENT:
|
||||
try:
|
||||
from app.services.referral_contest_service import referral_contest_service
|
||||
@@ -101,11 +102,7 @@ async def create_transaction(
|
||||
amount_kopeks,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
'Не удалось записать событие конкурса для пользователя %s: %s',
|
||||
user_id,
|
||||
exc,
|
||||
)
|
||||
logger.debug('Не удалось записать событие конкурса для пользователя', user_id=user_id, exc=exc)
|
||||
|
||||
return transaction
|
||||
|
||||
@@ -171,7 +168,7 @@ async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Tr
|
||||
await db.commit()
|
||||
await db.refresh(transaction)
|
||||
|
||||
logger.info(f'✅ Транзакция {transaction.id} завершена')
|
||||
logger.info('✅ Транзакция завершена', transaction_id=transaction.id)
|
||||
|
||||
try:
|
||||
from app.services.promo_group_assignment import (
|
||||
@@ -181,9 +178,7 @@ async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Tr
|
||||
await maybe_assign_promo_group_by_total_spent(db, transaction.user_id)
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
'Не удалось проверить автовыдачу промогруппы для пользователя %s: %s',
|
||||
transaction.user_id,
|
||||
exc,
|
||||
'Не удалось проверить автовыдачу промогруппы для пользователя', user_id=transaction.user_id, exc=exc
|
||||
)
|
||||
|
||||
return transaction
|
||||
@@ -392,7 +387,7 @@ async def check_tribute_payment_duplicate(
|
||||
transaction = result.scalar_one_or_none()
|
||||
|
||||
if transaction:
|
||||
logger.info(f'🔍 Найден дубликат платежа в течение 24ч: {transaction.id}')
|
||||
logger.info('🔍 Найден дубликат платежа в течение 24ч', transaction_id=transaction.id)
|
||||
|
||||
return transaction
|
||||
|
||||
@@ -408,7 +403,7 @@ async def create_unique_tribute_transaction(
|
||||
timestamp = int(datetime.utcnow().timestamp())
|
||||
external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}'
|
||||
|
||||
logger.info(f'Создан уникальный external_id для избежания дубликатов: {external_id}')
|
||||
logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id)
|
||||
|
||||
return await create_transaction(
|
||||
db=db,
|
||||
|
||||
+55
-52
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, case, func, nullslast, or_, select, text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -25,7 +25,7 @@ from app.database.models import (
|
||||
from app.utils.validators import sanitize_telegram_name
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def _normalize_language_code(language: str | None, fallback: str = 'ru') -> str:
|
||||
@@ -269,7 +269,11 @@ async def create_user_no_commit(
|
||||
user.promo_group = default_group
|
||||
|
||||
# Не коммитим сразу, оставляем для пакетной обработки
|
||||
logger.info(f'✅ Подготовлен пользователь {telegram_id} с реферальным кодом {referral_code} (ожидает коммита)')
|
||||
logger.info(
|
||||
'✅ Подготовлен пользователь с реферальным кодом (ожидает коммита)',
|
||||
telegram_id=telegram_id,
|
||||
referral_code=referral_code,
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@@ -316,7 +320,9 @@ async def create_user(
|
||||
await db.refresh(user)
|
||||
|
||||
user.promo_group = default_group
|
||||
logger.info(f'✅ Создан пользователь {telegram_id} с реферальным кодом {referral_code}')
|
||||
logger.info(
|
||||
'✅ Создан пользователь с реферальным кодом', telegram_id=telegram_id, referral_code=referral_code
|
||||
)
|
||||
|
||||
# Отправляем событие о создании пользователя
|
||||
try:
|
||||
@@ -336,7 +342,7 @@ async def create_user(
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning('Failed to emit user.created event: %s', error)
|
||||
logger.warning('Failed to emit user.created event', error=error)
|
||||
|
||||
return user
|
||||
|
||||
@@ -349,11 +355,10 @@ async def create_user(
|
||||
and attempt < attempts
|
||||
):
|
||||
logger.warning(
|
||||
'⚠️ Обнаружено несоответствие последовательности users_id_seq при создании пользователя %s. '
|
||||
'Выполняем повторную синхронизацию (попытка %s/%s)',
|
||||
telegram_id,
|
||||
attempt,
|
||||
attempts,
|
||||
'⚠️ Обнаружено несоответствие последовательности users_id_seq при создании пользователя . Выполняем повторную синхронизацию (попытка /)',
|
||||
telegram_id=telegram_id,
|
||||
attempt=attempt,
|
||||
attempts=attempts,
|
||||
)
|
||||
await _sync_users_sequence(db)
|
||||
continue
|
||||
@@ -413,7 +418,11 @@ async def add_user_balance(
|
||||
|
||||
user_id_display = user.telegram_id or user.email or f'#{user.id}'
|
||||
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,
|
||||
)
|
||||
|
||||
# Автоматическое возобновление приостановленной суточной подписки
|
||||
@@ -436,8 +445,9 @@ async def add_user_balance(
|
||||
if daily_price > 0 and user.balance_kopeks >= daily_price:
|
||||
await resume_daily_subscription(db, subscription)
|
||||
logger.info(
|
||||
f'✅ Автоматически возобновлена суточная подписка {subscription.id} '
|
||||
f'после пополнения баланса (user_id={user.id})'
|
||||
'✅ Автоматически возобновлена суточная подписка после пополнения баланса (user_id=)',
|
||||
subscription_id=subscription.id,
|
||||
user_id=user.id,
|
||||
)
|
||||
# Синхронизируем с RemnaWave
|
||||
try:
|
||||
@@ -446,14 +456,14 @@ async def add_user_balance(
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
except Exception as sync_err:
|
||||
logger.warning(f'Не удалось синхронизировать с RemnaWave: {sync_err}')
|
||||
logger.warning('Не удалось синхронизировать с RemnaWave', sync_err=sync_err)
|
||||
except Exception as resume_err:
|
||||
logger.warning(f'Ошибка при попытке возобновить суточную подписку: {resume_err}')
|
||||
logger.warning('Ошибка при попытке возобновить суточную подписку', resume_err=resume_err)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка изменения баланса пользователя {user.id}: {e}')
|
||||
logger.error('Ошибка изменения баланса пользователя', user_id=user.id, error=e)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -469,7 +479,7 @@ async def add_user_balance_by_id(
|
||||
try:
|
||||
user = await get_user_by_telegram_id(db, telegram_id)
|
||||
if not user:
|
||||
logger.error(f'Пользователь с telegram_id {telegram_id} не найден')
|
||||
logger.error('Пользователь с telegram_id не найден', telegram_id=telegram_id)
|
||||
return False
|
||||
|
||||
return await add_user_balance(
|
||||
@@ -482,7 +492,7 @@ async def add_user_balance_by_id(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка пополнения баланса пользователя {telegram_id}: {e}')
|
||||
logger.error('Ошибка пополнения баланса пользователя', telegram_id=telegram_id, error=e)
|
||||
return False
|
||||
|
||||
|
||||
@@ -498,10 +508,10 @@ async def subtract_user_balance(
|
||||
) -> bool:
|
||||
user_id_display = user.telegram_id or user.email or f'#{user.id}'
|
||||
logger.info('💸 ОТЛАДКА subtract_user_balance:')
|
||||
logger.info(f' 👤 User ID: {user.id} (ID: {user_id_display})')
|
||||
logger.info(f' 💰 Баланс до списания: {user.balance_kopeks} копеек')
|
||||
logger.info(f' 💸 Сумма к списанию: {amount_kopeks} копеек')
|
||||
logger.info(f' 📝 Описание: {description}')
|
||||
logger.info('👤 User ID: (ID: )', user_id=user.id, user_id_display=user_id_display)
|
||||
logger.info('💰 Баланс до списания: копеек', balance_kopeks=user.balance_kopeks)
|
||||
logger.info('💸 Сумма к списанию: копеек', amount_kopeks=amount_kopeks)
|
||||
logger.info('📝 Описание', description=description)
|
||||
|
||||
# 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())
|
||||
@@ -531,9 +541,7 @@ async def subtract_user_balance(
|
||||
offer = await get_latest_claimed_offer_for_user(db, user.id, source)
|
||||
except Exception as lookup_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to fetch latest claimed promo offer for user %s: %s',
|
||||
user.id,
|
||||
lookup_error,
|
||||
'Failed to fetch latest claimed promo offer for user', user_id=user.id, lookup_error=lookup_error
|
||||
)
|
||||
offer = None
|
||||
|
||||
@@ -592,23 +600,21 @@ async def subtract_user_balance(
|
||||
)
|
||||
except Exception as log_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to record promo offer consumption log for user %s: %s',
|
||||
user.id,
|
||||
log_error,
|
||||
'Failed to record promo offer consumption log for user', user_id=user.id, log_error=log_error
|
||||
)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception as rollback_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to rollback session after promo offer consumption log failure: %s',
|
||||
rollback_error,
|
||||
'Failed to rollback session after promo offer consumption log failure',
|
||||
rollback_error=rollback_error,
|
||||
)
|
||||
|
||||
logger.info(f' ✅ Средства списаны: {old_balance} → {user.balance_kopeks}')
|
||||
logger.info('✅ Средства списаны: →', old_balance=old_balance, balance_kopeks=user.balance_kopeks)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f' ❌ ОШИБКА СПИСАНИЯ: {e}')
|
||||
logger.error('❌ ОШИБКА СПИСАНИЯ', error=e)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -643,9 +649,9 @@ async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
|
||||
offer = await get_latest_claimed_offer_for_user(db, user.id, source)
|
||||
except Exception as lookup_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to fetch latest claimed promo offer for user %s during expiration cleanup: %s',
|
||||
user.id,
|
||||
lookup_error,
|
||||
'Failed to fetch latest claimed promo offer for user during expiration cleanup',
|
||||
user_id=user.id,
|
||||
lookup_error=lookup_error,
|
||||
)
|
||||
offer = None
|
||||
|
||||
@@ -688,17 +694,12 @@ async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
|
||||
details={'reason': 'offer_expired'},
|
||||
)
|
||||
except Exception as log_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to log promo offer expiration for user %s: %s',
|
||||
user_id,
|
||||
log_error,
|
||||
)
|
||||
logger.warning('Failed to log promo offer expiration for user', user_id=user_id, log_error=log_error)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception as rollback_error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
'Failed to rollback session after promo offer expiration log failure: %s',
|
||||
rollback_error,
|
||||
'Failed to rollback session after promo offer expiration log failure', rollback_error=rollback_error
|
||||
)
|
||||
|
||||
return len(users)
|
||||
@@ -944,7 +945,7 @@ async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[Us
|
||||
),
|
||||
)
|
||||
else:
|
||||
logger.warning('Неизвестный сегмент для промо: %s', segment)
|
||||
logger.warning('Неизвестный сегмент для промо', segment=segment)
|
||||
return []
|
||||
|
||||
result = await db.execute(query.order_by(User.id))
|
||||
@@ -989,7 +990,7 @@ async def delete_user(db: AsyncSession, user: User) -> bool:
|
||||
|
||||
await db.commit()
|
||||
user_id_display = user.telegram_id or user.email or f'#{user.id}'
|
||||
logger.info(f'🗑️ Пользователь {user_id_display} помечен как удаленный')
|
||||
logger.info('🗑️ Пользователь помечен как удаленный', user_id_display=user_id_display)
|
||||
return True
|
||||
|
||||
|
||||
@@ -1104,7 +1105,7 @@ async def create_user_by_email(
|
||||
await db.refresh(user)
|
||||
|
||||
user.promo_group = default_group
|
||||
logger.info(f'✅ Создан email-пользователь {email} с id={user.id}')
|
||||
logger.info('✅ Создан email-пользователь с id', email=email, user_id=user.id)
|
||||
|
||||
# Emit event
|
||||
try:
|
||||
@@ -1123,7 +1124,7 @@ async def create_user_by_email(
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning('Failed to emit user.created event: %s', error)
|
||||
logger.warning('Failed to emit user.created event', error=error)
|
||||
|
||||
return user
|
||||
|
||||
@@ -1181,7 +1182,7 @@ async def set_email_change_pending(
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
logger.info(f'Email change pending for user {user.id}: {user.email} -> {new_email}')
|
||||
logger.info('Email change pending for user', user_id=user.id, email=user.email, new_email=new_email)
|
||||
return user
|
||||
|
||||
|
||||
@@ -1235,7 +1236,7 @@ async def verify_and_apply_email_change(db: AsyncSession, user: User, code: str)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
logger.info(f'Email changed for user {user.id}: {old_email} -> {new_email}')
|
||||
logger.info('Email changed for user', user_id=user.id, old_email=old_email, new_email=new_email)
|
||||
return True, 'Email changed successfully'
|
||||
|
||||
|
||||
@@ -1253,7 +1254,7 @@ async def clear_email_change_pending(db: AsyncSession, user: User) -> None:
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
logger.info(f'Email change cancelled for user {user.id}')
|
||||
logger.info('Email change cancelled for user', user_id=user.id)
|
||||
|
||||
|
||||
# --- OAuth provider functions ---
|
||||
@@ -1286,7 +1287,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
|
||||
setattr(user, column_name, value)
|
||||
user.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
logger.info(f'Linked {provider} (id={provider_id}) to user {user.id}')
|
||||
logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id)
|
||||
|
||||
|
||||
async def create_user_by_oauth(
|
||||
@@ -1332,7 +1333,9 @@ async def create_user_by_oauth(
|
||||
await db.refresh(user)
|
||||
|
||||
user.promo_group = default_group
|
||||
logger.info(f'Created OAuth user via {provider} (provider_id={provider_id}) with id={user.id}')
|
||||
logger.info(
|
||||
'Created OAuth user via (provider_id=) with id', provider=provider, provider_id=provider_id, user_id=user.id
|
||||
)
|
||||
|
||||
try:
|
||||
from app.services.event_emitter import event_emitter
|
||||
@@ -1349,6 +1352,6 @@ async def create_user_by_oauth(
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning('Failed to emit user.created event: %s', error)
|
||||
logger.warning('Failed to emit user.created event', error=error)
|
||||
|
||||
return user
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_user_message(
|
||||
@@ -36,7 +36,7 @@ async def create_user_message(
|
||||
await db.commit()
|
||||
await db.refresh(message)
|
||||
|
||||
logger.info(f'✅ Создано сообщение ID {message.id} пользователем {created_by}')
|
||||
logger.info('✅ Создано сообщение ID пользователем', message_id=message.id, created_by=created_by)
|
||||
return message
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ async def update_user_message(
|
||||
await db.commit()
|
||||
await db.refresh(message)
|
||||
|
||||
logger.info(f'📝 Обновлено сообщение ID {message_id}')
|
||||
logger.info('📝 Обновлено сообщение ID', message_id=message_id)
|
||||
return message
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ async def toggle_user_message_status(db: AsyncSession, message_id: int) -> UserM
|
||||
await db.refresh(message)
|
||||
|
||||
status_text = 'активировано' if message.is_active else 'деактивировано'
|
||||
logger.info(f'🔄 Сообщение ID {message_id} {status_text}')
|
||||
logger.info('🔄 Сообщение ID', message_id=message_id, status_text=status_text)
|
||||
|
||||
return message
|
||||
|
||||
@@ -147,7 +147,7 @@ async def delete_user_message(db: AsyncSession, message_id: int) -> bool:
|
||||
await db.delete(message)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'🗑️ Удалено сообщение ID {message_id}')
|
||||
logger.info('🗑️ Удалено сообщение ID', message_id=message_id)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""CRUD операции для связи пользователей с промогруппами (Many-to-Many)."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, desc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -10,7 +10,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import PromoGroup, User, UserPromoGroup
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def _sync_user_primary_promo_group(
|
||||
@@ -39,11 +39,7 @@ async def _sync_user_primary_promo_group(
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
'Ошибка синхронизации primary промогруппы пользователя %s: %s',
|
||||
user_id,
|
||||
error,
|
||||
)
|
||||
logger.error('Ошибка синхронизации primary промогруппы пользователя', user_id=user_id, error=error)
|
||||
|
||||
|
||||
async def sync_user_primary_promo_group(
|
||||
@@ -74,7 +70,7 @@ async def add_user_to_promo_group(
|
||||
# Проверяем существование связи
|
||||
existing = await has_user_promo_group(db, user_id, promo_group_id)
|
||||
if existing:
|
||||
logger.info(f'Пользователь {user_id} уже имеет промогруппу {promo_group_id}')
|
||||
logger.info('Пользователь уже имеет промогруппу', user_id=user_id, promo_group_id=promo_group_id)
|
||||
return None
|
||||
|
||||
# Создаем новую связь
|
||||
@@ -91,11 +87,16 @@ async def add_user_to_promo_group(
|
||||
await db.commit()
|
||||
await db.refresh(user_promo_group)
|
||||
|
||||
logger.info(f'Пользователю {user_id} добавлена промогруппа {promo_group_id} ({assigned_by})')
|
||||
logger.info(
|
||||
'Пользователю добавлена промогруппа',
|
||||
user_id=user_id,
|
||||
promo_group_id=promo_group_id,
|
||||
assigned_by=assigned_by,
|
||||
)
|
||||
return user_promo_group
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка добавления промогруппы пользователю: {error}')
|
||||
logger.error('Ошибка добавления промогруппы пользователю', error=error)
|
||||
await db.rollback()
|
||||
return None
|
||||
|
||||
@@ -121,7 +122,9 @@ async def remove_user_from_promo_group(db: AsyncSession, user_id: int, promo_gro
|
||||
user_promo_group = result.scalar_one_or_none()
|
||||
|
||||
if not user_promo_group:
|
||||
logger.warning(f'Связь пользователя {user_id} с промогруппой {promo_group_id} не найдена')
|
||||
logger.warning(
|
||||
'Связь пользователя с промогруппой не найдена', user_id=user_id, promo_group_id=promo_group_id
|
||||
)
|
||||
return False
|
||||
|
||||
await db.delete(user_promo_group)
|
||||
@@ -131,11 +134,11 @@ async def remove_user_from_promo_group(db: AsyncSession, user_id: int, promo_gro
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f'У пользователя {user_id} удалена промогруппа {promo_group_id}')
|
||||
logger.info('У пользователя удалена промогруппа', user_id=user_id, promo_group_id=promo_group_id)
|
||||
return True
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка удаления промогруппы у пользователя: {error}')
|
||||
logger.error('Ошибка удаления промогруппы у пользователя', error=error)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -162,7 +165,7 @@ async def get_user_promo_groups(db: AsyncSession, user_id: int) -> list[UserProm
|
||||
return list(result.scalars().all())
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка получения промогрупп пользователя {user_id}: {error}')
|
||||
logger.error('Ошибка получения промогрупп пользователя', user_id=user_id, error=error)
|
||||
return []
|
||||
|
||||
|
||||
@@ -187,7 +190,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
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка получения primary промогруппы пользователя {user_id}: {error}')
|
||||
logger.error('Ошибка получения primary промогруппы пользователя', user_id=user_id, error=error)
|
||||
return None
|
||||
|
||||
|
||||
@@ -212,7 +215,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
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка проверки промогруппы пользователя: {error}')
|
||||
logger.error('Ошибка проверки промогруппы пользователя', error=error)
|
||||
return False
|
||||
|
||||
|
||||
@@ -232,7 +235,7 @@ async def count_user_promo_groups(db: AsyncSession, user_id: int) -> int:
|
||||
return len(list(result.scalars().all()))
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка подсчета промогрупп пользователя: {error}')
|
||||
logger.error('Ошибка подсчета промогрупп пользователя', error=error)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -264,10 +267,10 @@ async def replace_user_promo_groups(
|
||||
db.add(user_promo_group)
|
||||
|
||||
await db.commit()
|
||||
logger.info(f'Промогруппы пользователя {user_id} заменены на {promo_group_ids}')
|
||||
logger.info('Промогруппы пользователя заменены на', user_id=user_id, promo_group_ids=promo_group_ids)
|
||||
return True
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка замены промогрупп пользователя: {error}')
|
||||
logger.error('Ошибка замены промогрупп пользователя', error=error)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
+12
-14
@@ -1,16 +1,16 @@
|
||||
"""CRUD helpers for WATA payment records."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import WataPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_wata_payment(
|
||||
@@ -53,11 +53,11 @@ async def create_wata_payment(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Создан Wata платеж #%s для пользователя %s: %s копеек (статус %s)',
|
||||
payment.id,
|
||||
user_id,
|
||||
amount_kopeks,
|
||||
status,
|
||||
'Создан Wata платеж # для пользователя : копеек (статус)',
|
||||
payment_id=payment.id,
|
||||
user_id=user_id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
status=status,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -128,10 +128,10 @@ async def update_wata_payment_status(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Обновлен Wata платеж %s: статус=%s, is_paid=%s',
|
||||
payment.payment_link_id,
|
||||
payment.status,
|
||||
payment.is_paid,
|
||||
'Обновлен Wata платеж : статус is_paid',
|
||||
payment_link_id=payment.payment_link_id,
|
||||
payment_status=payment.status,
|
||||
is_paid=payment.is_paid,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -147,9 +147,7 @@ async def link_wata_payment_to_transaction(
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
'Wata платеж %s привязан к транзакции %s',
|
||||
payment.payment_link_id,
|
||||
transaction_id,
|
||||
'Wata платеж привязан к транзакции', payment_link_id=payment.payment_link_id, transaction_id=transaction_id
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import User, WelcomeText
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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)
|
||||
|
||||
status = 'включен' if welcome_text.is_enabled else 'отключен'
|
||||
logger.info(f'Приветственный текст {status} администратором {admin_id}')
|
||||
logger.info('Приветственный текст администратором', status=status, admin_id=admin_id)
|
||||
return welcome_text.is_enabled
|
||||
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)
|
||||
@@ -92,11 +92,11 @@ async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
|
||||
await db.commit()
|
||||
await db.refresh(new_welcome_text)
|
||||
|
||||
logger.info(f'Создан и включен дефолтный приветственный текст администратором {admin_id}')
|
||||
logger.info('Создан и включен дефолтный приветственный текст администратором', admin_id=admin_id)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка при переключении статуса приветственного текста: {e}')
|
||||
logger.error('Ошибка при переключении статуса приветственного текста', error=e)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -116,11 +116,11 @@ async def set_welcome_text(db: AsyncSession, text_content: str, admin_id: int) -
|
||||
await db.commit()
|
||||
await db.refresh(new_welcome_text)
|
||||
|
||||
logger.info(f'Установлен новый приветственный текст администратором {admin_id}')
|
||||
logger.info('Установлен новый приветственный текст администратором', admin_id=admin_id)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка при установке приветственного текста: {e}')
|
||||
logger.error('Ошибка при установке приветственного текста', error=e)
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -154,10 +154,10 @@ async def create_welcome_text(
|
||||
await db.refresh(welcome_text)
|
||||
|
||||
logger.info(
|
||||
'✅ Создан приветственный текст ID %s (активный=%s, включен=%s)',
|
||||
welcome_text.id,
|
||||
welcome_text.is_active,
|
||||
welcome_text.is_enabled,
|
||||
'✅ Создан приветственный текст ID (активный включен=)',
|
||||
welcome_text_id=welcome_text.id,
|
||||
is_active=welcome_text.is_active,
|
||||
is_enabled=welcome_text.is_enabled,
|
||||
)
|
||||
return welcome_text
|
||||
|
||||
@@ -188,10 +188,10 @@ async def update_welcome_text(
|
||||
await db.refresh(welcome_text)
|
||||
|
||||
logger.info(
|
||||
'📝 Обновлен приветственный текст ID %s (активный=%s, включен=%s)',
|
||||
welcome_text.id,
|
||||
welcome_text.is_active,
|
||||
welcome_text.is_enabled,
|
||||
'📝 Обновлен приветственный текст ID (активный включен=)',
|
||||
welcome_text_id=welcome_text.id,
|
||||
is_active=welcome_text.is_active,
|
||||
is_enabled=welcome_text.is_enabled,
|
||||
)
|
||||
return welcome_text
|
||||
|
||||
@@ -199,7 +199,7 @@ async def update_welcome_text(
|
||||
async def delete_welcome_text(db: AsyncSession, welcome_text: WelcomeText) -> None:
|
||||
await db.delete(welcome_text)
|
||||
await db.commit()
|
||||
logger.info('🗑️ Удален приветственный текст ID %s', welcome_text.id)
|
||||
logger.info('🗑️ Удален приветственный текст ID', welcome_text_id=welcome_text.id)
|
||||
|
||||
|
||||
async def get_current_welcome_text_or_default() -> str:
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
CRUD операции для колеса удачи (Fortune Wheel).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, desc, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -17,7 +17,7 @@ from app.database.models import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# ==================== WHEEL CONFIG ====================
|
||||
@@ -68,7 +68,7 @@ async def update_wheel_config(db: AsyncSession, **kwargs) -> WheelConfig:
|
||||
config.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(config)
|
||||
logger.info(f'🎡 Обновлена конфигурация колеса: {kwargs}')
|
||||
logger.info('🎡 Обновлена конфигурация колеса', kwargs=kwargs)
|
||||
return config
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ async def create_wheel_prize(
|
||||
db.add(prize)
|
||||
await db.commit()
|
||||
await db.refresh(prize)
|
||||
logger.info(f'🎁 Создан приз колеса: {display_name} ({prize_type})')
|
||||
logger.info('🎁 Создан приз колеса', display_name=display_name, prize_type=prize_type)
|
||||
return prize
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ async def update_wheel_prize(db: AsyncSession, prize_id: int, **kwargs) -> Wheel
|
||||
prize.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(prize)
|
||||
logger.info(f'🎁 Обновлен приз колеса ID={prize_id}: {kwargs}')
|
||||
logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs)
|
||||
return prize
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ async def delete_wheel_prize(db: AsyncSession, prize_id: int) -> bool:
|
||||
|
||||
await db.delete(prize)
|
||||
await db.commit()
|
||||
logger.info(f'🗑️ Удален приз колеса ID={prize_id}')
|
||||
logger.info('🗑️ Удален приз колеса ID', prize_id=prize_id)
|
||||
return True
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ async def reorder_wheel_prizes(db: AsyncSession, prize_ids: list[int]) -> bool:
|
||||
prize.sort_order = index
|
||||
|
||||
await db.commit()
|
||||
logger.info(f'🔄 Переупорядочены призы колеса: {prize_ids}')
|
||||
logger.info('🔄 Переупорядочены призы колеса', prize_ids=prize_ids)
|
||||
return True
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ async def create_wheel_spin(
|
||||
db.add(spin)
|
||||
await db.commit()
|
||||
await db.refresh(spin)
|
||||
logger.info(f"🎰 Создан спин колеса: user_id={user_id}, prize='{prize_display_name}'")
|
||||
logger.info('🎰 Создан спин колеса: user_id=, prize', user_id=user_id, prize_display_name=prize_display_name)
|
||||
return spin
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import and_, select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import YooKassaPayment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_yookassa_payment(
|
||||
@@ -46,15 +46,20 @@ async def create_yookassa_payment(
|
||||
except IntegrityError as e:
|
||||
await db.rollback()
|
||||
logger.error(
|
||||
'FK violation при создании платежа YooKassa %s: user_id=%s не существует в БД: %s',
|
||||
yookassa_payment_id,
|
||||
user_id,
|
||||
e,
|
||||
'FK violation при создании платежа YooKassa : user_id= не существует в БД',
|
||||
yookassa_payment_id=yookassa_payment_id,
|
||||
user_id=user_id,
|
||||
e=e,
|
||||
)
|
||||
return None
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(f'Создан платеж YooKassa: {yookassa_payment_id} на {amount_kopeks / 100}₽ для пользователя {user_id}')
|
||||
logger.info(
|
||||
'Создан платеж YooKassa: на ₽ для пользователя',
|
||||
yookassa_payment_id=yookassa_payment_id,
|
||||
amount_kopeks=amount_kopeks / 100,
|
||||
user_id=user_id,
|
||||
)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -104,7 +109,12 @@ async def update_yookassa_payment_status(
|
||||
payment = result.scalar_one_or_none()
|
||||
|
||||
if payment:
|
||||
logger.info(f'Обновлен статус платежа YooKassa {yookassa_payment_id}: {status}, paid={is_paid}')
|
||||
logger.info(
|
||||
'Обновлен статус платежа YooKassa , paid',
|
||||
yookassa_payment_id=yookassa_payment_id,
|
||||
status=status,
|
||||
is_paid=is_paid,
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -127,7 +137,11 @@ async def link_yookassa_payment_to_transaction(
|
||||
payment = result.scalar_one_or_none()
|
||||
|
||||
if payment:
|
||||
logger.info(f'Платеж YooKassa {yookassa_payment_id} связан с транзакцией {transaction_id}')
|
||||
logger.info(
|
||||
'Платеж YooKassa связан с транзакцией',
|
||||
yookassa_payment_id=yookassa_payment_id,
|
||||
transaction_id=transaction_id,
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
@@ -185,7 +199,7 @@ async def delete_yookassa_payment(db: AsyncSession, yookassa_payment_id: str) ->
|
||||
if payment:
|
||||
await db.delete(payment)
|
||||
await db.commit()
|
||||
logger.info(f'Удален платеж YooKassa: {yookassa_payment_id}')
|
||||
logger.info('Удален платеж YooKassa', yookassa_payment_id=yookassa_payment_id)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+22
-24
@@ -1,11 +1,11 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import wraps
|
||||
from typing import TypeVar
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import bindparam, event, inspect, text
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.exc import InterfaceError, OperationalError, ProgrammingError
|
||||
@@ -16,7 +16,7 @@ from app.config import settings
|
||||
from app.database.models import Base
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
@@ -126,16 +126,16 @@ def with_db_retry(
|
||||
last_exception = e
|
||||
if attempt < attempts:
|
||||
logger.warning(
|
||||
'Ошибка БД (попытка %d/%d): %s. Повтор через %.1f сек...',
|
||||
attempt,
|
||||
attempts,
|
||||
str(e)[:100],
|
||||
current_delay,
|
||||
'Ошибка БД (попытка /): . Повтор через сек...',
|
||||
attempt=attempt,
|
||||
attempts=attempts,
|
||||
e=str(e)[:100],
|
||||
current_delay=current_delay,
|
||||
)
|
||||
await asyncio.sleep(current_delay)
|
||||
current_delay *= backoff
|
||||
else:
|
||||
logger.error('Ошибка БД: все %d попыток исчерпаны. Последняя ошибка: %s', attempts, str(e))
|
||||
logger.error('Ошибка БД: все попыток исчерпаны. Последняя ошибка', attempts=attempts, e=str(e))
|
||||
|
||||
raise last_exception
|
||||
|
||||
@@ -159,7 +159,7 @@ async def execute_with_retry(
|
||||
except RETRYABLE_EXCEPTIONS as e:
|
||||
last_exception = e
|
||||
if attempt < attempts:
|
||||
logger.warning('SQL retry (попытка %d/%d): %s', attempt, attempts, str(e)[:100])
|
||||
logger.warning('SQL retry (попытка /)', attempt=attempt, attempts=attempts, e=str(e)[:100])
|
||||
await asyncio.sleep(delay)
|
||||
delay *= 2
|
||||
|
||||
@@ -175,15 +175,15 @@ if settings.DEBUG:
|
||||
@event.listens_for(Engine, 'before_cursor_execute')
|
||||
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
||||
conn.info.setdefault('query_start_time', []).append(time.time())
|
||||
logger.debug(f'🔍 Executing query: {statement[:100]}...')
|
||||
logger.debug('🔍 Executing query: ...', statement=statement[:100])
|
||||
|
||||
@event.listens_for(Engine, 'after_cursor_execute')
|
||||
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
||||
total = time.time() - conn.info['query_start_time'].pop(-1)
|
||||
if total > 0.1: # Логируем медленные запросы > 100ms
|
||||
logger.warning(f'🐌 Slow query ({total:.3f}s): {statement[:100]}...')
|
||||
logger.warning('🐌 Slow query (s): ...', total=round(total, 3), statement=statement[:100])
|
||||
else:
|
||||
logger.debug(f'⚡ Query executed in {total:.3f}s')
|
||||
logger.debug('⚡ Query executed in', total=round(total, 3))
|
||||
|
||||
# ============================================================================
|
||||
# ADVANCED SESSION MANAGER WITH READ REPLICAS
|
||||
@@ -201,7 +201,7 @@ def _validate_database_url(url: str | None) -> str | None:
|
||||
return None
|
||||
# Простая проверка на валидный формат
|
||||
if not ('://' in url or url.startswith('sqlite')):
|
||||
logger.warning('Невалидный DATABASE_URL: %s', url[:20])
|
||||
logger.warning('Невалидный DATABASE_URL', url=url[:20])
|
||||
return None
|
||||
return url
|
||||
|
||||
@@ -234,9 +234,9 @@ class DatabaseManager:
|
||||
expire_on_commit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
logger.info('Read replica настроена: %s', replica_url[:30] + '...')
|
||||
logger.info('Read replica настроена', replica_url=replica_url[:30] + '...')
|
||||
except Exception as e:
|
||||
logger.error('Не удалось настроить read replica: %s', e)
|
||||
logger.error('Не удалось настроить read replica', e=e)
|
||||
self.read_replica_engine = None
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -276,10 +276,10 @@ class DatabaseManager:
|
||||
latency = (time.time() - start) * 1000
|
||||
status = 'healthy'
|
||||
except TimeoutError:
|
||||
logger.error('Health check таймаут (%s сек)', timeout)
|
||||
logger.error('Health check таймаут (сек)', timeout=timeout)
|
||||
status = 'timeout'
|
||||
except Exception as e:
|
||||
logger.error('Database health check failed: %s', e)
|
||||
logger.error('Database health check failed', e=e)
|
||||
status = 'unhealthy'
|
||||
|
||||
return {
|
||||
@@ -307,7 +307,7 @@ class DatabaseManager:
|
||||
except TimeoutError:
|
||||
status = 'timeout'
|
||||
except Exception as e:
|
||||
logger.error('Read replica health check failed: %s', e)
|
||||
logger.error('Read replica health check failed', e=e)
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
@@ -440,7 +440,7 @@ async def init_db():
|
||||
# Продолжаем выполнение, так как основные таблицы могут быть созданы
|
||||
else:
|
||||
# Для других ошибок пробрасываем исключение
|
||||
logger.error(f'❌ Ошибка при создании таблиц: {e}')
|
||||
logger.error('❌ Ошибка при создании таблиц', error=e)
|
||||
raise
|
||||
|
||||
if not IS_SQLITE:
|
||||
@@ -468,21 +468,19 @@ async def init_db():
|
||||
|
||||
if not table_exists:
|
||||
logger.debug(
|
||||
'Пропускаем создание индекса %s: таблица %s отсутствует',
|
||||
index_sql,
|
||||
table_name,
|
||||
'Пропускаем создание индекса : таблица отсутствует', index_sql=index_sql, table_name=table_name
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
await conn.execute(text(index_sql))
|
||||
except Exception as e:
|
||||
logger.debug('Index creation skipped for %s: %s', table_name, e)
|
||||
logger.debug('Index creation skipped for', table_name=table_name, e=e)
|
||||
|
||||
logger.info('База данных успешно инициализирована')
|
||||
|
||||
health = await db_manager.health_check()
|
||||
logger.info('Database health: %s', health)
|
||||
logger.info('Database health', health=health)
|
||||
|
||||
|
||||
async def close_db():
|
||||
|
||||
+207
-207
File diff suppressed because it is too large
Load Diff
Vendored
+4
-4
@@ -4,13 +4,13 @@ Ban System API Client.
|
||||
Client for interacting with the BedolagaBan monitoring system.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import structlog
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class BanSystemAPIError(Exception):
|
||||
@@ -78,7 +78,7 @@ class BanSystemAPI:
|
||||
response_text = await response.text()
|
||||
|
||||
if response.status >= 400:
|
||||
logger.error(f'Ban System API error: {response.status} - {response_text}')
|
||||
logger.error('Ban System API error', status=response.status, response_text=response_text)
|
||||
raise BanSystemAPIError(
|
||||
message=f'API error {response.status}: {response_text}',
|
||||
status_code=response.status,
|
||||
@@ -93,7 +93,7 @@ class BanSystemAPI:
|
||||
return {}
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f'Ban System API connection error: {e}')
|
||||
logger.error('Ban System API connection error', error=e)
|
||||
raise BanSystemAPIError(message=f'Connection error: {e!s}', status_code=None, response_data=None)
|
||||
except TimeoutError:
|
||||
logger.error('Ban System API request timeout')
|
||||
|
||||
Vendored
+8
-8
@@ -1,14 +1,14 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import structlog
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class CryptoBotService:
|
||||
@@ -49,11 +49,11 @@ class CryptoBotService:
|
||||
|
||||
if response.status == 200 and response_data.get('ok'):
|
||||
return response_data.get('result')
|
||||
logger.error(f'CryptoBot API ошибка: {response_data}')
|
||||
logger.error('CryptoBot API ошибка', response_data=response_data)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка запроса к CryptoBot API: {e}')
|
||||
logger.error('Ошибка запроса к CryptoBot API', error=e)
|
||||
return None
|
||||
|
||||
async def get_me(self) -> dict[str, Any] | None:
|
||||
@@ -81,7 +81,7 @@ class CryptoBotService:
|
||||
result = await self._make_request('POST', 'createInvoice', data)
|
||||
|
||||
if result:
|
||||
logger.info(f'Создан CryptoBot invoice {result.get("invoice_id")} на {amount} {asset}')
|
||||
logger.info('Создан CryptoBot invoice на', get=result.get('invoice_id'), amount=amount, asset=asset)
|
||||
|
||||
return result
|
||||
|
||||
@@ -140,7 +140,7 @@ class CryptoBotService:
|
||||
return is_valid
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка проверки подписи CryptoBot webhook: {e}')
|
||||
logger.error('Ошибка проверки подписи CryptoBot webhook', error=e)
|
||||
return False
|
||||
|
||||
async def process_webhook(self, webhook_data: dict[str, Any]) -> dict[str, Any] | None:
|
||||
@@ -161,9 +161,9 @@ class CryptoBotService:
|
||||
'payment_system': 'cryptobot',
|
||||
}
|
||||
|
||||
logger.warning(f'Неизвестный тип CryptoBot webhook: {update_type}')
|
||||
logger.warning('Неизвестный тип CryptoBot webhook', update_type=update_type)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка обработки CryptoBot webhook: {e}')
|
||||
logger.error('Ошибка обработки CryptoBot webhook', error=e)
|
||||
return None
|
||||
|
||||
Vendored
+13
-9
@@ -6,15 +6,15 @@ import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import structlog
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class HeleketService:
|
||||
@@ -93,26 +93,28 @@ class HeleketService:
|
||||
):
|
||||
text = await response.text()
|
||||
if response.content_type != 'application/json':
|
||||
logger.error('Ответ Heleket не JSON (%s): %s', response.content_type, text)
|
||||
logger.error('Ответ Heleket не JSON', content_type=response.content_type, text=text)
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
logger.error('Ошибка парсинга Heleket JSON: %s', text)
|
||||
logger.error('Ошибка парсинга Heleket JSON', text=text)
|
||||
return None
|
||||
|
||||
if response.status >= 400:
|
||||
logger.error('Heleket API %s вернул статус %s: %s', endpoint, response.status, data)
|
||||
logger.error(
|
||||
'Heleket API вернул статус', endpoint=endpoint, response_status=response.status, data=data
|
||||
)
|
||||
return None
|
||||
|
||||
if isinstance(data, dict) and data.get('state') == 0:
|
||||
return data
|
||||
|
||||
logger.error('Heleket API вернул ошибку: %s', data)
|
||||
logger.error('Heleket API вернул ошибку', data=data)
|
||||
return None
|
||||
except Exception as error: # pragma: no cover - defensive
|
||||
logger.error('Ошибка запроса к Heleket API: %s', error)
|
||||
logger.error('Ошибка запроса к Heleket API', error=error)
|
||||
return None
|
||||
|
||||
async def create_payment(self, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
@@ -157,7 +159,7 @@ class HeleketService:
|
||||
return True
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
logger.error('Heleket webhook payload не dict: %s', payload)
|
||||
logger.error('Heleket webhook payload не dict', payload=payload)
|
||||
return False
|
||||
|
||||
signature = payload.get('sign')
|
||||
@@ -173,5 +175,7 @@ class HeleketService:
|
||||
is_valid = hmac.compare_digest(expected, str(signature))
|
||||
|
||||
if not is_valid:
|
||||
logger.error('Неверная подпись Heleket webhook: ожидается %s, получено %s', expected, signature)
|
||||
logger.error(
|
||||
'Неверная подпись Heleket webhook: ожидается , получено', expected=expected, signature=signature
|
||||
)
|
||||
return is_valid
|
||||
|
||||
Vendored
+10
-10
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from aiohttp import web
|
||||
|
||||
from app.config import settings
|
||||
@@ -11,7 +11,7 @@ from app.external.heleket import HeleketService
|
||||
from app.services.payment_service import PaymentService
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class HeleketWebhookHandler:
|
||||
@@ -39,7 +39,7 @@ class HeleketWebhookHandler:
|
||||
processed = await self.payment_service.process_heleket_webhook(db, payload)
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка обработки Heleket webhook: {e}')
|
||||
logger.error('Ошибка обработки Heleket webhook', error=e)
|
||||
await db.rollback()
|
||||
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:
|
||||
await site.start()
|
||||
logger.info(
|
||||
'Heleket webhook сервер запущен на %s:%s',
|
||||
settings.HELEKET_WEBHOOK_HOST,
|
||||
settings.HELEKET_WEBHOOK_PORT,
|
||||
'Heleket webhook сервер запущен на',
|
||||
HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST,
|
||||
HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT,
|
||||
)
|
||||
logger.info(
|
||||
'Heleket webhook URL: http://%s:%s%s',
|
||||
settings.HELEKET_WEBHOOK_HOST,
|
||||
settings.HELEKET_WEBHOOK_PORT,
|
||||
settings.HELEKET_WEBHOOK_PATH,
|
||||
'Heleket webhook URL: http://',
|
||||
HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST,
|
||||
HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT,
|
||||
HELEKET_WEBHOOK_PATH=settings.HELEKET_WEBHOOK_PATH,
|
||||
)
|
||||
|
||||
while True:
|
||||
|
||||
Vendored
+6
-15
@@ -3,17 +3,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import structlog
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class Pal24APIError(Exception):
|
||||
@@ -96,30 +96,21 @@ class Pal24Client:
|
||||
payload = await response.json(content_type=None)
|
||||
except aiohttp.ContentTypeError:
|
||||
text_body = await response.text()
|
||||
logger.error(
|
||||
'Pal24 API returned non-JSON response for %s: %s',
|
||||
endpoint,
|
||||
text_body,
|
||||
)
|
||||
logger.error('Pal24 API returned non-JSON response for', endpoint=endpoint, text_body=text_body)
|
||||
raise Pal24APIError(f'Pal24 API returned non-JSON response: {text_body}') from None
|
||||
|
||||
result = Pal24Response.from_payload(payload, status)
|
||||
if status >= 400 or not result.success:
|
||||
logger.error(
|
||||
'Pal24 API error %s %s: %s',
|
||||
status,
|
||||
endpoint,
|
||||
payload,
|
||||
)
|
||||
logger.error('Pal24 API error', status=status, endpoint=endpoint, payload=payload)
|
||||
result.raise_for_status(endpoint)
|
||||
|
||||
return result
|
||||
|
||||
except TimeoutError as error:
|
||||
logger.error('Pal24 API request timeout for %s: %s', endpoint, error)
|
||||
logger.error('Pal24 API request timeout for', endpoint=endpoint, error=error)
|
||||
raise Pal24APIError(f'Pal24 API request timeout for {endpoint}') from error
|
||||
except aiohttp.ClientError as error:
|
||||
logger.error('Pal24 API client error for %s: %s', endpoint, error)
|
||||
logger.error('Pal24 API client error for', endpoint=endpoint, error=error)
|
||||
raise Pal24APIError(str(error)) from error
|
||||
|
||||
# API methods -----------------------------------------------------------------
|
||||
|
||||
Vendored
+28
-28
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import ssl
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
@@ -10,9 +9,10 @@ from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiohttp
|
||||
import structlog
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class UserStatus(Enum):
|
||||
@@ -310,7 +310,7 @@ class RemnaWaveAPI:
|
||||
async def __aenter__(self):
|
||||
conn_type = self._detect_connection_type()
|
||||
|
||||
logger.debug(f'Подключение к Remnawave: {self.base_url} (тип: {conn_type})')
|
||||
logger.debug('Подключение к Remnawave: (тип: )', base_url=self.base_url, conn_type=conn_type)
|
||||
|
||||
headers = self._prepare_auth_headers()
|
||||
|
||||
@@ -319,10 +319,10 @@ class RemnaWaveAPI:
|
||||
if ':' in self.secret_key:
|
||||
key_name, key_value = self.secret_key.split(':', 1)
|
||||
cookies = {key_name: key_value}
|
||||
logger.debug(f'Используем куки: {key_name}=***')
|
||||
logger.debug('Используем куки: =***', key_name=key_name)
|
||||
else:
|
||||
cookies = {self.secret_key: self.secret_key}
|
||||
logger.debug(f'Используем куки: {self.secret_key}=***')
|
||||
logger.debug('Используем куки: =***', secret_key=self.secret_key)
|
||||
|
||||
connector_kwargs = {}
|
||||
|
||||
@@ -388,12 +388,12 @@ class RemnaWaveAPI:
|
||||
if response.status == 429 and attempt < max_retries:
|
||||
retry_after = float(response.headers.get('Retry-After', base_delay * (2**attempt)))
|
||||
logger.warning(
|
||||
'Rate limited (429) on %s %s, retry %d/%d after %.1fs',
|
||||
method,
|
||||
endpoint,
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
retry_after,
|
||||
'Rate limited (429) on , retry / after s',
|
||||
method=method,
|
||||
endpoint=endpoint,
|
||||
attempt=attempt + 1,
|
||||
max_retries=max_retries,
|
||||
retry_after=retry_after,
|
||||
)
|
||||
await asyncio.sleep(retry_after)
|
||||
continue
|
||||
@@ -411,17 +411,17 @@ class RemnaWaveAPI:
|
||||
if attempt < max_retries:
|
||||
delay = base_delay * (2**attempt)
|
||||
logger.warning(
|
||||
'Request failed on %s %s: %s, retry %d/%d after %.1fs',
|
||||
method,
|
||||
endpoint,
|
||||
e,
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
delay,
|
||||
'Request failed on retry / after s',
|
||||
method=method,
|
||||
endpoint=endpoint,
|
||||
e=e,
|
||||
attempt=attempt + 1,
|
||||
max_retries=max_retries,
|
||||
delay=delay,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
continue
|
||||
logger.error(f'Request failed: {e}')
|
||||
logger.error('Request failed', error=e)
|
||||
raise RemnaWaveAPIError(f'Request failed: {e!s}')
|
||||
|
||||
raise RemnaWaveAPIError(f'Max retries exceeded for {method} {endpoint}')
|
||||
@@ -461,7 +461,7 @@ class RemnaWaveAPI:
|
||||
if active_internal_squads:
|
||||
data['activeInternalSquads'] = active_internal_squads
|
||||
|
||||
logger.debug('Создание пользователя в панели: %s', data)
|
||||
logger.debug('Создание пользователя в панели', data=data)
|
||||
response = await self._make_request('POST', '/api/users', data)
|
||||
user = self._parse_user(response['response'])
|
||||
return await self.enrich_user_with_happ_link(user)
|
||||
@@ -1049,13 +1049,13 @@ class RemnaWaveAPI:
|
||||
delete_data = {'userUuid': user_uuid, 'hwid': device_hwid}
|
||||
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
|
||||
except Exception as device_error:
|
||||
logger.error(f'Ошибка удаления устройства {device_hwid}: {device_error}')
|
||||
logger.error('Ошибка удаления устройства', device_hwid=device_hwid, device_error=device_error)
|
||||
failed_count += 1
|
||||
|
||||
return failed_count < len(devices) / 2
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка при сбросе устройств: {e}')
|
||||
logger.error('Ошибка при сбросе устройств', error=e)
|
||||
return False
|
||||
|
||||
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)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка удаления устройства {device_hwid}: {e}')
|
||||
logger.error('Ошибка удаления устройства', device_hwid=device_hwid, error=e)
|
||||
return False
|
||||
|
||||
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)
|
||||
return response.get('response', {}).get('encryptedLink')
|
||||
except RemnaWaveAPIError as e:
|
||||
logger.warning(f'Не удалось зашифровать happ ссылку: {e.message}')
|
||||
logger.warning('Не удалось зашифровать happ ссылку', message=e.message)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f'Ошибка при шифровании happ ссылки: {e}')
|
||||
logger.warning('Ошибка при шифровании happ ссылки', error=e)
|
||||
return None
|
||||
|
||||
async def enrich_user_with_happ_link(self, user: RemnaWaveUser) -> RemnaWaveUser:
|
||||
@@ -1112,7 +1112,7 @@ class RemnaWaveAPI:
|
||||
try:
|
||||
status = UserStatus(status_str)
|
||||
except ValueError:
|
||||
logger.warning(f'Неизвестный статус пользователя: {status_str}, используем ACTIVE')
|
||||
logger.warning('Неизвестный статус пользователя: используем ACTIVE', status_str=status_str)
|
||||
status = UserStatus.ACTIVE
|
||||
|
||||
# Получаем trafficLimitStrategy с fallback
|
||||
@@ -1120,7 +1120,7 @@ class RemnaWaveAPI:
|
||||
try:
|
||||
traffic_strategy = TrafficLimitStrategy(strategy_str)
|
||||
except ValueError:
|
||||
logger.warning(f'Неизвестная стратегия трафика: {strategy_str}, используем NO_RESET')
|
||||
logger.warning('Неизвестная стратегия трафика: используем NO_RESET', strategy_str=strategy_str)
|
||||
traffic_strategy = TrafficLimitStrategy.NO_RESET
|
||||
|
||||
return RemnaWaveUser(
|
||||
@@ -1285,5 +1285,5 @@ async def test_api_connection(api: RemnaWaveAPI) -> bool:
|
||||
await api.get_system_stats()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning('API connection test failed: %s', e)
|
||||
logger.warning('API connection test failed', e=e)
|
||||
return False
|
||||
|
||||
Vendored
+16
-10
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from aiogram import Bot
|
||||
from aiogram.types import InlineKeyboardMarkup, LabeledPrice
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TelegramStarsService:
|
||||
@@ -49,13 +49,16 @@ class TelegramStarsService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'Создан Stars invoice на {stars_amount} звезд (~{settings.format_price(amount_kopeks)}) '
|
||||
f'для {chat_id}, курс: {stars_rate}₽/⭐'
|
||||
'Создан Stars invoice на звезд (~) для курс: ₽/⭐',
|
||||
stars_amount=stars_amount,
|
||||
settings=settings.format_price(amount_kopeks),
|
||||
chat_id=chat_id,
|
||||
stars_rate=stars_rate,
|
||||
)
|
||||
return invoice_link
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка создания Stars invoice: {e}')
|
||||
logger.error('Ошибка создания Stars invoice', error=e)
|
||||
return None
|
||||
|
||||
async def send_invoice(
|
||||
@@ -84,8 +87,11 @@ class TelegramStarsService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'Отправлен Stars invoice {message.message_id} на {stars_amount} звезд '
|
||||
f'(~{settings.format_price(amount_kopeks)}), курс: {stars_rate}₽/⭐'
|
||||
'Отправлен Stars invoice на звезд (~), курс: ₽/⭐',
|
||||
message_id=message.message_id,
|
||||
stars_amount=stars_amount,
|
||||
settings=settings.format_price(amount_kopeks),
|
||||
stars_rate=stars_rate,
|
||||
)
|
||||
return {
|
||||
'message_id': message.message_id,
|
||||
@@ -95,7 +101,7 @@ class TelegramStarsService:
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка отправки Stars invoice: {e}')
|
||||
logger.error('Ошибка отправки Stars invoice', error=e)
|
||||
return None
|
||||
|
||||
async def answer_pre_checkout_query(
|
||||
@@ -105,8 +111,8 @@ class TelegramStarsService:
|
||||
await self.bot.answer_pre_checkout_query(
|
||||
pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message
|
||||
)
|
||||
logger.info(f'Ответ на pre_checkout_query: ok={ok}')
|
||||
logger.info('Ответ на pre_checkout_query: ok', ok=ok)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка ответа на pre_checkout_query: {e}')
|
||||
logger.error('Ошибка ответа на pre_checkout_query', error=e)
|
||||
return False
|
||||
|
||||
Vendored
+24
-17
@@ -1,13 +1,14 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TributeService:
|
||||
@@ -25,11 +26,11 @@ class TributeService:
|
||||
try:
|
||||
payment_url = f'{self.donate_link}&user_id={user_id}'
|
||||
|
||||
logger.info(f'Создана ссылка Tribute для пользователя {user_id}')
|
||||
logger.info('Создана ссылка Tribute для пользователя', user_id=user_id)
|
||||
return payment_url
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка создания Tribute ссылки: {e}')
|
||||
logger.error('Ошибка создания Tribute ссылки', error=e)
|
||||
return None
|
||||
|
||||
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
|
||||
@@ -50,7 +51,7 @@ class TributeService:
|
||||
return is_valid
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка проверки подписи webhook: {e}')
|
||||
logger.error('Ошибка проверки подписи webhook', error=e)
|
||||
return False
|
||||
|
||||
async def process_webhook(self, payload_or_data) -> dict[str, Any] | None:
|
||||
@@ -60,9 +61,9 @@ class TributeService:
|
||||
if isinstance(payload_or_data, str):
|
||||
try:
|
||||
webhook_data = json.loads(payload_or_data)
|
||||
logger.info(f'📊 Распарсенные данные: {webhook_data}')
|
||||
logger.info('📊 Распарсенные данные', webhook_data=webhook_data)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f'❌ Ошибка парсинга JSON: {e}')
|
||||
logger.error('❌ Ошибка парсинга JSON', error=e)
|
||||
return None
|
||||
else:
|
||||
webhook_data = payload_or_data
|
||||
@@ -99,18 +100,24 @@ class TributeService:
|
||||
status = 'unknown'
|
||||
|
||||
logger.info(
|
||||
f'📝 Извлеченные данные: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}'
|
||||
'📝 Извлеченные данные: payment_id=, status=, amount_kopeks=, user_id',
|
||||
payment_id=payment_id,
|
||||
status=status,
|
||||
amount_kopeks=amount_kopeks,
|
||||
telegram_user_id=telegram_user_id,
|
||||
)
|
||||
|
||||
if not telegram_user_id:
|
||||
logger.error('❌ Не найден telegram_user_id в webhook данных')
|
||||
logger.error(f'🔍 Полные данные для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}')
|
||||
logger.error(
|
||||
'🔍 Полные данные для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2)
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
telegram_user_id = int(telegram_user_id)
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f'❌ Некорректный telegram_user_id: {telegram_user_id}')
|
||||
logger.error('❌ Некорректный telegram_user_id', telegram_user_id=telegram_user_id)
|
||||
return None
|
||||
|
||||
result = {
|
||||
@@ -123,28 +130,28 @@ class TributeService:
|
||||
'payment_system': 'tribute',
|
||||
}
|
||||
|
||||
logger.info(f'✅ Tribute webhook обработан успешно: {result}')
|
||||
logger.info('✅ Tribute webhook обработан успешно', result=result)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Ошибка обработки Tribute webhook: {e}', exc_info=True)
|
||||
logger.error(f'🔍 Webhook data для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}')
|
||||
logger.error('❌ Ошибка обработки Tribute webhook', error=e, exc_info=True)
|
||||
logger.error('🔍 Webhook data для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2))
|
||||
return None
|
||||
|
||||
async def get_payment_status(self, payment_id: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
logger.info(f'Запрос статуса платежа {payment_id}')
|
||||
logger.info('Запрос статуса платежа', payment_id=payment_id)
|
||||
return {'status': 'unknown', 'payment_id': payment_id}
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка получения статуса платежа: {e}')
|
||||
logger.error('Ошибка получения статуса платежа', error=e)
|
||||
return None
|
||||
|
||||
async def refund_payment(
|
||||
self, payment_id: str, amount_kopeks: int | None = None, reason: str = 'Возврат по запросу'
|
||||
) -> dict[str, Any] | None:
|
||||
try:
|
||||
logger.info(f'Создание возврата для платежа {payment_id}')
|
||||
logger.info('Создание возврата для платежа', payment_id=payment_id)
|
||||
return {'refund_id': f'refund_{payment_id}', 'status': 'pending'}
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка создания возврата: {e}')
|
||||
logger.error('Ошибка создания возврата', error=e)
|
||||
return None
|
||||
|
||||
Vendored
+21
-28
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import structlog
|
||||
from aiohttp import web
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
@@ -18,7 +18,7 @@ from app.database.database import AsyncSessionLocal
|
||||
from app.services.payment_service import PaymentService
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class WataPublicKeyProvider:
|
||||
@@ -67,28 +67,24 @@ class WataPublicKeyProvider:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session, session.get(url) as response:
|
||||
text = await response.text()
|
||||
if response.status >= 400:
|
||||
logger.error(
|
||||
'Ошибка получения публичного ключа WATA %s: %s',
|
||||
response.status,
|
||||
text,
|
||||
)
|
||||
logger.error('Ошибка получения публичного ключа WATA', response_status=response.status, text=text)
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = await response.json()
|
||||
except aiohttp.ContentTypeError:
|
||||
logger.error('Ответ WATA public-key не является JSON: %s', text)
|
||||
logger.error('Ответ WATA public-key не является JSON', text=text)
|
||||
return None
|
||||
|
||||
if isinstance(payload, dict):
|
||||
value = payload.get('value')
|
||||
if value:
|
||||
return value
|
||||
logger.error('Ответ WATA public-key не содержит ключ: %s', payload)
|
||||
logger.error('Ответ WATA public-key не содержит ключ', payload=payload)
|
||||
else:
|
||||
logger.error('Неожиданный формат ответа WATA public-key: %s', payload)
|
||||
logger.error('Неожиданный формат ответа WATA public-key', payload=payload)
|
||||
except Exception as error:
|
||||
logger.error('Ошибка запроса публичного ключа WATA: %s', error)
|
||||
logger.error('Ошибка запроса публичного ключа WATA', error=error)
|
||||
|
||||
return None
|
||||
|
||||
@@ -125,7 +121,7 @@ class WataWebhookHandler:
|
||||
try:
|
||||
public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
|
||||
except ValueError as error:
|
||||
logger.error('Ошибка загрузки публичного ключа WATA: %s', error)
|
||||
logger.error('Ошибка загрузки публичного ключа WATA', error=error)
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -140,7 +136,7 @@ class WataWebhookHandler:
|
||||
logger.warning('Подпись WATA webhook не прошла проверку')
|
||||
return False
|
||||
except Exception as error:
|
||||
logger.error('Ошибка проверки подписи WATA: %s', error)
|
||||
logger.error('Ошибка проверки подписи WATA', error=error)
|
||||
return False
|
||||
|
||||
async def handle_webhook(self, request: web.Request) -> web.Response:
|
||||
@@ -164,9 +160,9 @@ class WataWebhookHandler:
|
||||
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
|
||||
|
||||
logger.info(
|
||||
'Получен WATA webhook: order_id=%s, status=%s',
|
||||
payload.get('orderId'),
|
||||
payload.get('transactionStatus'),
|
||||
'Получен WATA webhook: order_id status',
|
||||
payload=payload.get('orderId'),
|
||||
payload_2=payload.get('transactionStatus'),
|
||||
)
|
||||
|
||||
processed: bool | None = None
|
||||
@@ -175,7 +171,7 @@ class WataWebhookHandler:
|
||||
processed = await self.payment_service.process_wata_webhook(db, payload)
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка обработки WATA webhook: {e}')
|
||||
logger.error('Ошибка обработки WATA webhook', error=e)
|
||||
await db.rollback()
|
||||
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
|
||||
|
||||
@@ -218,10 +214,7 @@ def create_wata_webhook_app(payment_service: PaymentService) -> web.Application:
|
||||
app.router.add_options(settings.WATA_WEBHOOK_PATH, handler.options_handler)
|
||||
app.router.add_get('/health', handler.health_check)
|
||||
|
||||
logger.info(
|
||||
'Настроен WATA webhook endpoint на %s',
|
||||
settings.WATA_WEBHOOK_PATH,
|
||||
)
|
||||
logger.info('Настроен WATA webhook endpoint на', WATA_WEBHOOK_PATH=settings.WATA_WEBHOOK_PATH)
|
||||
|
||||
return app
|
||||
|
||||
@@ -244,15 +237,15 @@ async def start_wata_webhook_server(payment_service: PaymentService) -> None:
|
||||
try:
|
||||
await site.start()
|
||||
logger.info(
|
||||
'WATA webhook сервер запущен на %s:%s',
|
||||
settings.WATA_WEBHOOK_HOST,
|
||||
settings.WATA_WEBHOOK_PORT,
|
||||
'WATA webhook сервер запущен на',
|
||||
WATA_WEBHOOK_HOST=settings.WATA_WEBHOOK_HOST,
|
||||
WATA_WEBHOOK_PORT=settings.WATA_WEBHOOK_PORT,
|
||||
)
|
||||
logger.info(
|
||||
'WATA webhook URL: http://%s:%s%s',
|
||||
settings.WATA_WEBHOOK_HOST,
|
||||
settings.WATA_WEBHOOK_PORT,
|
||||
settings.WATA_WEBHOOK_PATH,
|
||||
'WATA webhook URL: http://',
|
||||
WATA_WEBHOOK_HOST=settings.WATA_WEBHOOK_HOST,
|
||||
WATA_WEBHOOK_PORT=settings.WATA_WEBHOOK_PORT,
|
||||
WATA_WEBHOOK_PATH=settings.WATA_WEBHOOK_PATH,
|
||||
)
|
||||
|
||||
while True:
|
||||
|
||||
Vendored
+62
-79
@@ -2,9 +2,9 @@ import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
|
||||
import structlog
|
||||
from aiogram import Bot
|
||||
from aiohttp import web
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.services.payment_service import PaymentService
|
||||
from app.services.tribute_service import TributeService
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class WebhookServer:
|
||||
@@ -51,8 +51,8 @@ class WebhookServer:
|
||||
missing.append('FREEKASSA_SECRET_WORD_2')
|
||||
if missing:
|
||||
logger.warning(
|
||||
f'Freekassa ENABLED=true, но webhook не зарегистрирован. '
|
||||
f'Отсутствуют параметры: {", ".join(missing)}'
|
||||
'Freekassa ENABLED=true, но webhook не зарегистрирован. Отсутствуют параметры',
|
||||
value=', '.join(missing),
|
||||
)
|
||||
|
||||
self.app.router.add_get('/health', self._health_check)
|
||||
@@ -66,18 +66,16 @@ class WebhookServer:
|
||||
self.app.router.add_options(settings.FREEKASSA_WEBHOOK_PATH, self._options_handler)
|
||||
|
||||
logger.info('Webhook сервер настроен:')
|
||||
logger.info(f' - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}')
|
||||
logger.info('Tribute webhook: POST', TRIBUTE_WEBHOOK_PATH=settings.TRIBUTE_WEBHOOK_PATH)
|
||||
if settings.is_mulenpay_enabled():
|
||||
mulenpay_name = settings.get_mulenpay_display_name()
|
||||
logger.info(
|
||||
' - %s webhook: POST %s',
|
||||
mulenpay_name,
|
||||
settings.MULENPAY_WEBHOOK_PATH,
|
||||
'- webhook: POST', mulenpay_name=mulenpay_name, MULENPAY_WEBHOOK_PATH=settings.MULENPAY_WEBHOOK_PATH
|
||||
)
|
||||
if settings.is_cryptobot_enabled():
|
||||
logger.info(f' - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}')
|
||||
logger.info('CryptoBot webhook: POST', CRYPTOBOT_WEBHOOK_PATH=settings.CRYPTOBOT_WEBHOOK_PATH)
|
||||
if settings.is_freekassa_enabled():
|
||||
logger.info(f' - Freekassa webhook: POST {settings.FREEKASSA_WEBHOOK_PATH}')
|
||||
logger.info('Freekassa webhook: POST', FREEKASSA_WEBHOOK_PATH=settings.FREEKASSA_WEBHOOK_PATH)
|
||||
logger.info(' - Health check: GET /health')
|
||||
|
||||
return self.app
|
||||
@@ -95,35 +93,35 @@ class WebhookServer:
|
||||
await self.site.start()
|
||||
|
||||
logger.info(
|
||||
'Webhook сервер запущен на %s:%s',
|
||||
settings.TRIBUTE_WEBHOOK_HOST,
|
||||
settings.TRIBUTE_WEBHOOK_PORT,
|
||||
'Webhook сервер запущен на',
|
||||
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
|
||||
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
|
||||
)
|
||||
logger.info(
|
||||
'Tribute webhook URL: http://%s:%s%s',
|
||||
settings.TRIBUTE_WEBHOOK_HOST,
|
||||
settings.TRIBUTE_WEBHOOK_PORT,
|
||||
settings.TRIBUTE_WEBHOOK_PATH,
|
||||
'Tribute webhook URL: http://',
|
||||
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
|
||||
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
|
||||
TRIBUTE_WEBHOOK_PATH=settings.TRIBUTE_WEBHOOK_PATH,
|
||||
)
|
||||
if settings.is_mulenpay_enabled():
|
||||
mulenpay_name = settings.get_mulenpay_display_name()
|
||||
logger.info(
|
||||
'%s webhook URL: http://%s:%s%s',
|
||||
mulenpay_name,
|
||||
settings.TRIBUTE_WEBHOOK_HOST,
|
||||
settings.TRIBUTE_WEBHOOK_PORT,
|
||||
settings.MULENPAY_WEBHOOK_PATH,
|
||||
'webhook URL: http://',
|
||||
mulenpay_name=mulenpay_name,
|
||||
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
|
||||
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
|
||||
MULENPAY_WEBHOOK_PATH=settings.MULENPAY_WEBHOOK_PATH,
|
||||
)
|
||||
if settings.is_cryptobot_enabled():
|
||||
logger.info(
|
||||
'CryptoBot webhook URL: http://%s:%s%s',
|
||||
settings.TRIBUTE_WEBHOOK_HOST,
|
||||
settings.TRIBUTE_WEBHOOK_PORT,
|
||||
settings.CRYPTOBOT_WEBHOOK_PATH,
|
||||
'CryptoBot webhook URL: http://',
|
||||
TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
|
||||
TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
|
||||
CRYPTOBOT_WEBHOOK_PATH=settings.CRYPTOBOT_WEBHOOK_PATH,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка запуска webhook сервера: {e}')
|
||||
logger.error('Ошибка запуска webhook сервера', error=e)
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
@@ -137,7 +135,7 @@ class WebhookServer:
|
||||
logger.info('Webhook runner очищен')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка остановки webhook сервера: {e}')
|
||||
logger.error('Ошибка остановки webhook сервера', error=e)
|
||||
|
||||
async def _options_handler(self, request: web.Request) -> web.Response:
|
||||
return web.Response(
|
||||
@@ -152,36 +150,27 @@ class WebhookServer:
|
||||
async def _mulenpay_webhook_handler(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
mulenpay_name = settings.get_mulenpay_display_name()
|
||||
logger.info(
|
||||
'%s webhook: %s %s',
|
||||
mulenpay_name,
|
||||
request.method,
|
||||
request.path,
|
||||
)
|
||||
logger.info(
|
||||
'%s webhook headers: %s',
|
||||
mulenpay_name,
|
||||
dict(request.headers),
|
||||
)
|
||||
logger.info('webhook', mulenpay_name=mulenpay_name, method=request.method, request_path=request.path)
|
||||
logger.info('webhook headers', mulenpay_name=mulenpay_name, headers=dict(request.headers))
|
||||
raw_body = await request.read()
|
||||
|
||||
if not raw_body:
|
||||
logger.warning('Пустой %s webhook', mulenpay_name)
|
||||
logger.warning('Пустой webhook', mulenpay_name=mulenpay_name)
|
||||
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
|
||||
|
||||
# Временно отключаем проверку подписи для отладки
|
||||
# TODO: Включить обратно после настройки MulenPay
|
||||
if not self._verify_mulenpay_signature(request, raw_body):
|
||||
logger.warning(
|
||||
'%s webhook signature verification failed, but processing anyway for debugging',
|
||||
mulenpay_name,
|
||||
'webhook signature verification failed, but processing anyway for debugging',
|
||||
mulenpay_name=mulenpay_name,
|
||||
)
|
||||
# return web.json_response({"status": "error", "reason": "invalid_signature"}, status=401)
|
||||
|
||||
try:
|
||||
payload = json.loads(raw_body.decode('utf-8'))
|
||||
except json.JSONDecodeError as error:
|
||||
logger.error(f'Ошибка парсинга {mulenpay_name} webhook: {error}')
|
||||
logger.error('Ошибка парсинга webhook', mulenpay_name=mulenpay_name, error=error)
|
||||
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
|
||||
|
||||
payment_service = PaymentService(self.bot)
|
||||
@@ -196,10 +185,7 @@ class WebhookServer:
|
||||
return web.json_response({'status': 'ok'}, status=200)
|
||||
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f'Ошибка обработки {mulenpay_name} webhook: {error}',
|
||||
exc_info=True,
|
||||
)
|
||||
logger.error('Ошибка обработки webhook', mulenpay_name=mulenpay_name, error=error, exc_info=True)
|
||||
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
|
||||
finally:
|
||||
try:
|
||||
@@ -209,10 +195,7 @@ class WebhookServer:
|
||||
|
||||
except Exception as error:
|
||||
mulenpay_name = settings.get_mulenpay_display_name()
|
||||
logger.error(
|
||||
f'Критическая ошибка {mulenpay_name} webhook: {error}',
|
||||
exc_info=True,
|
||||
)
|
||||
logger.error('Критическая ошибка webhook', mulenpay_name=mulenpay_name, error=error, exc_info=True)
|
||||
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(error)}, status=500)
|
||||
|
||||
@staticmethod
|
||||
@@ -228,14 +211,14 @@ class WebhookServer:
|
||||
secret_key = settings.MULENPAY_SECRET_KEY
|
||||
display_name = settings.get_mulenpay_display_name()
|
||||
if not secret_key:
|
||||
logger.error('%s secret key is not configured', display_name)
|
||||
logger.error('secret key is not configured', display_name=display_name)
|
||||
return False
|
||||
|
||||
# Логируем все заголовки для отладки
|
||||
logger.info('%s webhook headers for signature verification:', display_name)
|
||||
logger.info('webhook headers for signature verification', display_name=display_name)
|
||||
for header_name, header_value in request.headers.items():
|
||||
if any(keyword in header_name.lower() for keyword in ['signature', 'sign', 'token', 'auth']):
|
||||
logger.info(f' {header_name}: {header_value}')
|
||||
logger.info('log event', header_name=header_name, header_value=header_value)
|
||||
|
||||
signature = WebhookServer._extract_mulenpay_header(
|
||||
request,
|
||||
@@ -283,7 +266,7 @@ class WebhookServer:
|
||||
if hmac.compare_digest(normalized_signature_no_padding, expected_urlsafe_base64_signature.rstrip('=')):
|
||||
return True
|
||||
|
||||
logger.error('Неверная подпись %s webhook', display_name)
|
||||
logger.error('Неверная подпись webhook', display_name=display_name)
|
||||
return False
|
||||
|
||||
authorization_header = request.headers.get('Authorization')
|
||||
@@ -296,7 +279,7 @@ class WebhookServer:
|
||||
if hmac.compare_digest(token, secret_key):
|
||||
return True
|
||||
|
||||
logger.error('Неверный %s токен %s webhook', scheme, display_name)
|
||||
logger.error('Неверный токен webhook', scheme=scheme, display_name=display_name)
|
||||
return False
|
||||
|
||||
if not value and hmac.compare_digest(token, secret_key):
|
||||
@@ -319,13 +302,13 @@ class WebhookServer:
|
||||
{key: value for key, value in request.headers.items() if 'authorization' not in key.lower()},
|
||||
)
|
||||
|
||||
logger.error('Отсутствует подпись %s webhook', display_name)
|
||||
logger.error('Отсутствует подпись webhook', display_name=display_name)
|
||||
return False
|
||||
|
||||
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
logger.info(f'Получен Tribute webhook: {request.method} {request.path}')
|
||||
logger.info(f'Headers: {dict(request.headers)}')
|
||||
logger.info('Получен Tribute webhook', method=request.method, path=request.path)
|
||||
logger.info('Headers', value=dict(request.headers))
|
||||
|
||||
raw_body = await request.read()
|
||||
|
||||
@@ -334,17 +317,17 @@ class WebhookServer:
|
||||
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
|
||||
|
||||
payload = raw_body.decode('utf-8')
|
||||
logger.info(f'Payload: {payload}')
|
||||
logger.info('Payload', payload=payload)
|
||||
|
||||
try:
|
||||
webhook_data = json.loads(payload)
|
||||
logger.info(f'Распарсенные данные: {webhook_data}')
|
||||
logger.info('Распарсенные данные', webhook_data=webhook_data)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f'Ошибка парсинга JSON: {e}')
|
||||
logger.error('Ошибка парсинга JSON', error=e)
|
||||
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
|
||||
|
||||
signature = request.headers.get('trbt-signature')
|
||||
logger.info(f'Signature: {signature}')
|
||||
logger.info('Signature', signature=signature)
|
||||
|
||||
if not signature:
|
||||
logger.error('Отсутствует заголовок подписи Tribute webhook')
|
||||
@@ -361,19 +344,19 @@ class WebhookServer:
|
||||
result = await self.tribute_service.process_webhook(payload)
|
||||
|
||||
if result:
|
||||
logger.info(f'Tribute webhook обработан успешно: {result}')
|
||||
logger.info('Tribute webhook обработан успешно', result=result)
|
||||
return web.json_response({'status': 'ok', 'result': result}, status=200)
|
||||
logger.error('Ошибка обработки Tribute webhook')
|
||||
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Критическая ошибка обработки Tribute webhook: {e}', exc_info=True)
|
||||
logger.error('Критическая ошибка обработки Tribute webhook', error=e, exc_info=True)
|
||||
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:
|
||||
try:
|
||||
logger.info(f'Получен CryptoBot webhook: {request.method} {request.path}')
|
||||
logger.info(f'Headers: {dict(request.headers)}')
|
||||
logger.info('Получен CryptoBot webhook', method=request.method, path=request.path)
|
||||
logger.info('Headers', value=dict(request.headers))
|
||||
|
||||
raw_body = await request.read()
|
||||
|
||||
@@ -382,17 +365,17 @@ class WebhookServer:
|
||||
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
|
||||
|
||||
payload = raw_body.decode('utf-8')
|
||||
logger.info(f'CryptoBot Payload: {payload}')
|
||||
logger.info('CryptoBot Payload', payload=payload)
|
||||
|
||||
try:
|
||||
webhook_data = json.loads(payload)
|
||||
logger.info(f'CryptoBot данные: {webhook_data}')
|
||||
logger.info('CryptoBot данные', webhook_data=webhook_data)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f'Ошибка парсинга CryptoBot JSON: {e}')
|
||||
logger.error('Ошибка парсинга CryptoBot JSON', error=e)
|
||||
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
|
||||
|
||||
signature = request.headers.get('Crypto-Pay-API-Signature')
|
||||
logger.info(f'CryptoBot Signature: {signature}')
|
||||
logger.info('CryptoBot Signature', signature=signature)
|
||||
|
||||
if signature and settings.CRYPTOBOT_WEBHOOK_SECRET:
|
||||
from app.external.cryptobot import CryptoBotService
|
||||
@@ -417,7 +400,7 @@ class WebhookServer:
|
||||
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Критическая ошибка обработки CryptoBot webhook: {e}', exc_info=True)
|
||||
logger.error('Критическая ошибка обработки CryptoBot webhook', error=e, exc_info=True)
|
||||
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(e)}, status=500)
|
||||
|
||||
async def _health_check(self, request: web.Request) -> web.Response:
|
||||
@@ -448,22 +431,22 @@ class WebhookServer:
|
||||
- CUR_ID: ID валюты/платежной системы
|
||||
"""
|
||||
try:
|
||||
logger.info(f'Получен Freekassa webhook: {request.method} {request.path}')
|
||||
logger.info('Получен Freekassa webhook', method=request.method, path=request.path)
|
||||
|
||||
# Получаем IP клиента
|
||||
client_ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip()
|
||||
if not client_ip:
|
||||
client_ip = request.remote or 'unknown'
|
||||
logger.info(f'Freekassa webhook IP: {client_ip}')
|
||||
logger.info('Freekassa webhook IP', client_ip=client_ip)
|
||||
|
||||
# Freekassa отправляет form-data
|
||||
try:
|
||||
form_data = await request.post()
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка парсинга Freekassa form-data: {e}')
|
||||
logger.error('Ошибка парсинга Freekassa form-data', error=e)
|
||||
return web.Response(text='NO', status=400)
|
||||
|
||||
logger.info(f'Freekassa webhook data: {dict(form_data)}')
|
||||
logger.info('Freekassa webhook data', value=dict(form_data))
|
||||
|
||||
# Извлекаем параметры
|
||||
merchant_id = int(form_data.get('MERCHANT_ID', 0))
|
||||
@@ -496,12 +479,12 @@ class WebhookServer:
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f'Freekassa webhook обработан успешно: order_id={order_id}')
|
||||
logger.info('Freekassa webhook обработан успешно: order_id', order_id=order_id)
|
||||
# Freekassa ожидает YES в ответе
|
||||
return web.Response(text='YES', status=200)
|
||||
logger.error(f'Ошибка обработки Freekassa webhook: order_id={order_id}')
|
||||
logger.error('Ошибка обработки Freekassa webhook: order_id', order_id=order_id)
|
||||
return web.Response(text='NO', status=400)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Критическая ошибка обработки Freekassa webhook: {e}', exc_info=True)
|
||||
logger.error('Критическая ошибка обработки Freekassa webhook', error=e, exc_info=True)
|
||||
return web.Response(text='NO', status=500)
|
||||
|
||||
Vendored
+33
-28
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
@@ -14,6 +13,7 @@ from ipaddress import (
|
||||
)
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import structlog
|
||||
from aiohttp import web
|
||||
|
||||
from app.config import settings
|
||||
@@ -23,7 +23,7 @@ from app.database.database import AsyncSessionLocal
|
||||
if TYPE_CHECKING:
|
||||
from app.services.payment_service import PaymentService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
IPAddress = Union[IPv4Address, IPv6Address]
|
||||
@@ -140,7 +140,7 @@ def _get_trusted_proxy_networks() -> tuple[IPNetwork, ...]:
|
||||
try:
|
||||
networks.append(ip_network(candidate, strict=False))
|
||||
except ValueError:
|
||||
logger.warning('Неверная сеть доверенного прокси YooKassa: %s', candidate)
|
||||
logger.warning('Неверная сеть доверенного прокси YooKassa', candidate=candidate)
|
||||
|
||||
cached_networks = tuple(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:
|
||||
try:
|
||||
logger.info(f'📥 Получен YooKassa webhook: {request.method} {request.path}')
|
||||
logger.info(f'📋 Headers: {dict(request.headers)}')
|
||||
logger.info('📥 Получен YooKassa webhook', method=request.method, path=request.path)
|
||||
logger.info('📋 Headers', value=dict(request.headers))
|
||||
|
||||
header_ip_candidates = collect_yookassa_ip_candidates(
|
||||
request.headers.get('X-Forwarded-For'),
|
||||
@@ -221,8 +221,8 @@ class YooKassaWebhookHandler:
|
||||
|
||||
if client_ip is None:
|
||||
logger.warning(
|
||||
'🚫 Не удалось определить IP-адрес отправителя YooKassa webhook. Кандидаты: %s',
|
||||
header_ip_candidates + ([request.remote] if request.remote else []),
|
||||
'🚫 Не удалось определить IP-адрес отправителя YooKassa webhook. Кандидаты',
|
||||
header_ip_candidates=header_ip_candidates + ([request.remote] if request.remote else []),
|
||||
)
|
||||
return web.Response(status=403, text='Forbidden')
|
||||
|
||||
@@ -234,7 +234,7 @@ class YooKassaWebhookHandler:
|
||||
)
|
||||
return web.Response(status=403, text='Forbidden')
|
||||
|
||||
logger.info('🌐 IP-адрес YooKassa подтверждён: %s', client_ip)
|
||||
logger.info('🌐 IP-адрес YooKassa подтверждён', client_ip=client_ip)
|
||||
|
||||
body = await request.text()
|
||||
|
||||
@@ -242,20 +242,20 @@ class YooKassaWebhookHandler:
|
||||
logger.warning('⚠️ Получен пустой webhook от YooKassa')
|
||||
return web.Response(status=400, text='Empty body')
|
||||
|
||||
logger.info(f'📄 Body: {body}')
|
||||
logger.info('📄 Body', body=body)
|
||||
|
||||
signature = request.headers.get('Signature') or request.headers.get('X-YooKassa-Signature')
|
||||
if signature:
|
||||
logger.info('ℹ️ Получена подпись YooKassa: %s', signature)
|
||||
logger.info('ℹ️ Получена подпись YooKassa', signature=signature)
|
||||
|
||||
try:
|
||||
webhook_data = json.loads(body)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f'❌ Ошибка парсинга JSON webhook YooKassa: {e}')
|
||||
logger.error('❌ Ошибка парсинга JSON webhook YooKassa', error=e)
|
||||
return web.Response(status=400, text='Invalid JSON')
|
||||
|
||||
logger.info(f'📊 Обработка webhook YooKassa: {webhook_data.get("event", "unknown_event")}')
|
||||
logger.debug(f'🔍 Полные данные webhook: {webhook_data}')
|
||||
logger.info('📊 Обработка webhook YooKassa', get=webhook_data.get('event', 'unknown_event'))
|
||||
logger.debug('🔍 Полные данные webhook', webhook_data=webhook_data)
|
||||
|
||||
event_type = webhook_data.get('event')
|
||||
if not event_type:
|
||||
@@ -269,7 +269,7 @@ class YooKassaWebhookHandler:
|
||||
return web.Response(status=400, text='No payment id')
|
||||
|
||||
if event_type not in YOOKASSA_ALLOWED_EVENTS:
|
||||
logger.info(f'ℹ️ Игнорируем событие YooKassa: {event_type}')
|
||||
logger.info('ℹ️ Игнорируем событие YooKassa', event_type=event_type)
|
||||
return web.Response(status=200, text='OK')
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
@@ -286,7 +286,8 @@ class YooKassaWebhookHandler:
|
||||
|
||||
if existing_transaction and event_type == 'payment.succeeded':
|
||||
logger.info(
|
||||
f'ℹ️ Платеж YooKassa {yookassa_payment_id} уже был обработан. Пропускаем дублирующий вебхук.'
|
||||
'ℹ️ Платеж YooKassa уже был обработан. Пропускаем дублирующий вебхук.',
|
||||
yookassa_payment_id=yookassa_payment_id,
|
||||
)
|
||||
return web.Response(status=200, text='OK')
|
||||
|
||||
@@ -295,22 +296,26 @@ class YooKassaWebhookHandler:
|
||||
if success:
|
||||
await db.commit()
|
||||
logger.info(
|
||||
f'✅ Успешно обработан webhook YooKassa: {event_type} для платежа {yookassa_payment_id}'
|
||||
'✅ Успешно обработан webhook YooKassa: для платежа',
|
||||
event_type=event_type,
|
||||
yookassa_payment_id=yookassa_payment_id,
|
||||
)
|
||||
return web.Response(status=200, text='OK')
|
||||
await db.rollback()
|
||||
logger.error(
|
||||
f'❌ Ошибка обработки webhook YooKassa: {event_type} для платежа {yookassa_payment_id}'
|
||||
'❌ Ошибка обработки webhook YooKassa: для платежа',
|
||||
event_type=event_type,
|
||||
yookassa_payment_id=yookassa_payment_id,
|
||||
)
|
||||
return web.Response(status=500, text='Processing error')
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f'❌ Ошибка обработки webhook YooKassa: {e}', exc_info=True)
|
||||
logger.error('❌ Ошибка обработки webhook YooKassa', error=e, exc_info=True)
|
||||
return web.Response(status=500, text='Processing error')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Критическая ошибка обработки webhook YooKassa: {e}', exc_info=True)
|
||||
logger.error('❌ Критическая ошибка обработки webhook YooKassa', error=e, exc_info=True)
|
||||
return web.Response(status=500, text='Internal server error')
|
||||
|
||||
def setup_routes(self, app: web.Application) -> None:
|
||||
@@ -319,7 +324,7 @@ class YooKassaWebhookHandler:
|
||||
app.router.add_get(webhook_path, self._get_handler)
|
||||
app.router.add_options(webhook_path, self._options_handler)
|
||||
|
||||
logger.info(f'✅ Настроен YooKassa webhook на пути: POST {webhook_path}')
|
||||
logger.info('✅ Настроен YooKassa webhook на пути: POST', webhook_path=webhook_path)
|
||||
|
||||
async def _get_handler(self, request: web.Request) -> web.Response:
|
||||
return web.json_response(
|
||||
@@ -381,15 +386,15 @@ async def start_yookassa_webhook_server(payment_service: PaymentService) -> None
|
||||
await site.start()
|
||||
|
||||
logger.info(
|
||||
'✅ YooKassa webhook сервер запущен на %s:%s',
|
||||
settings.YOOKASSA_WEBHOOK_HOST,
|
||||
settings.YOOKASSA_WEBHOOK_PORT,
|
||||
'✅ YooKassa webhook сервер запущен на',
|
||||
YOOKASSA_WEBHOOK_HOST=settings.YOOKASSA_WEBHOOK_HOST,
|
||||
YOOKASSA_WEBHOOK_PORT=settings.YOOKASSA_WEBHOOK_PORT,
|
||||
)
|
||||
logger.info(
|
||||
'🎯 YooKassa webhook URL: http://%s:%s%s',
|
||||
settings.YOOKASSA_WEBHOOK_HOST,
|
||||
settings.YOOKASSA_WEBHOOK_PORT,
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
'🎯 YooKassa webhook URL: http://',
|
||||
YOOKASSA_WEBHOOK_HOST=settings.YOOKASSA_WEBHOOK_HOST,
|
||||
YOOKASSA_WEBHOOK_PORT=settings.YOOKASSA_WEBHOOK_PORT,
|
||||
YOOKASSA_WEBHOOK_PATH=settings.YOOKASSA_WEBHOOK_PATH,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -403,5 +408,5 @@ async def start_yookassa_webhook_server(payment_service: PaymentService) -> None
|
||||
logger.info('✅ YooKassa webhook сервер остановлен')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Ошибка запуска YooKassa webhook сервера: {e}', exc_info=True)
|
||||
logger.error('❌ Ошибка запуска YooKassa webhook сервера', error=e, exc_info=True)
|
||||
raise
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user