diff --git a/app/database/crud/transaction.py b/app/database/crud/transaction.py
index ee7698db..97e96455 100644
--- a/app/database/crud/transaction.py
+++ b/app/database/crud/transaction.py
@@ -51,13 +51,17 @@ async def get_transaction_by_id(db: AsyncSession, transaction_id: int) -> Option
async def get_transaction_by_external_id(
db: AsyncSession,
- external_id: str
+ external_id: str,
+ payment_method: PaymentMethod
) -> Optional[Transaction]:
- """Получить транзакцию по external_id"""
result = await db.execute(
select(Transaction)
- .options(selectinload(Transaction.user))
- .where(Transaction.external_id == external_id)
+ .where(
+ and_(
+ Transaction.external_id == external_id,
+ Transaction.payment_method == payment_method.value
+ )
+ )
)
return result.scalar_one_or_none()
@@ -273,17 +277,16 @@ async def get_revenue_by_period(
return [{"date": row.date, "amount_kopeks": row.amount} for row in result]
-
async def find_tribute_transactions_by_payment_id(
db: AsyncSession,
payment_id: str,
user_telegram_id: Optional[int] = None
) -> List[Transaction]:
+ """Найти все Tribute транзакции по payment_id"""
query = select(Transaction).options(selectinload(Transaction.user))
conditions = [
- Transaction.external_id == f"tribute_{payment_id}",
Transaction.external_id == f"donation_{payment_id}",
Transaction.external_id == payment_id,
Transaction.external_id.like(f"%{payment_id}%")
@@ -331,14 +334,15 @@ async def create_unique_tribute_transaction(
amount_kopeks: int,
description: str
) -> Transaction:
+ """Создать уникальную Tribute транзакцию с защитой от дубликатов"""
- external_id = f"tribute_{payment_id}"
+ external_id = f"donation_{payment_id}"
- existing = await get_transaction_by_external_id(db, external_id)
+ existing = await get_transaction_by_external_id(db, external_id, PaymentMethod.TRIBUTE)
if existing:
timestamp = int(datetime.utcnow().timestamp())
- external_id = f"tribute_{payment_id}_{amount_kopeks}_{timestamp}"
+ external_id = f"donation_{payment_id}_{amount_kopeks}_{timestamp}"
logger.info(f"Создан уникальный external_id для избежания дубликатов: {external_id}")
@@ -352,70 +356,3 @@ async def create_unique_tribute_transaction(
external_id=external_id,
is_completed=True
)
-
-
-class TransactionCRUD:
-
- async def create_transaction(
- self,
- db: AsyncSession,
- transaction_data: dict
- ) -> Optional[Transaction]:
- try:
- transaction = Transaction(
- user_id=transaction_data['user_id'],
- type=transaction_data['transaction_type'],
- amount_kopeks=transaction_data['amount_kopeks'],
- description=transaction_data['description'],
- external_id=transaction_data.get('external_id'),
- payment_method=transaction_data.get('payment_system'),
- is_completed=transaction_data.get('status') == 'completed'
- )
-
- db.add(transaction)
- await db.commit()
- await db.refresh(transaction)
-
- logger.info(f"Создана транзакция: {transaction.id} на {transaction.amount_kopeks} коп.")
- return transaction
-
- except Exception as e:
- logger.error(f"Ошибка создания транзакции: {e}")
- await db.rollback()
- return None
-
- async def get_transaction_by_external_id(
- self,
- db: AsyncSession,
- external_id: str
- ) -> Optional[Transaction]:
- return await get_transaction_by_external_id(db, external_id)
-
- async def update_transaction_status(
- self,
- db: AsyncSession,
- transaction_id: int,
- status: str
- ) -> bool:
- try:
- result = await db.execute(
- select(Transaction).where(Transaction.id == transaction_id)
- )
- transaction = result.scalar_one_or_none()
-
- if not transaction:
- logger.error(f"Транзакция {transaction_id} не найдена")
- return False
-
- transaction.is_completed = (status == 'completed')
- if transaction.is_completed:
- transaction.completed_at = datetime.utcnow()
-
- await db.commit()
- logger.info(f"Статус транзакции {transaction_id} обновлен на {status}")
- return True
-
- except Exception as e:
- logger.error(f"Ошибка обновления статуса транзакции: {e}")
- await db.rollback()
- return False
diff --git a/app/database/crud/user.py b/app/database/crud/user.py
index bcf0787c..5c862226 100644
--- a/app/database/crud/user.py
+++ b/app/database/crud/user.py
@@ -156,25 +156,25 @@ async def add_user_balance(
async def add_user_balance_by_id(
db: AsyncSession,
- user_id: int,
+ telegram_id: int,
amount_kopeks: int,
description: str = "Пополнение баланса"
) -> bool:
- """Пополнить баланс пользователя по ID"""
try:
- user = await get_user_by_id(db, user_id)
-
+ user = await get_user_by_telegram_id(db, telegram_id)
if not user:
- user = await get_user_by_telegram_id(db, user_id)
-
- if not user:
- logger.error(f"Пользователь с ID {user_id} не найден")
+ logger.error(f"Пользователь с telegram_id {telegram_id} не найден")
return False
return await add_user_balance(db, user, amount_kopeks, description)
except Exception as e:
- logger.error(f"Ошибка пополнения баланса пользователя {user_id}: {e}")
+ logger.error(f"Ошибка пополнения баланса пользователя {telegram_id}: {e}")
+ return False
+
+ except Exception as e:
+ logger.error(f"❌ Ошибка пополнения баланса пользователя {user_id}: {e}")
+ await db.rollback()
return False
async def subtract_user_balance(
@@ -356,14 +356,3 @@ async def get_users_statistics(db: AsyncSession) -> dict:
"new_week": new_week,
"new_month": new_month
}
-
-class UserCRUD:
-
- async def get_user(self, db: AsyncSession, user_id: int) -> Optional[User]:
- return await get_user_by_id(db, user_id)
-
- async def get_user_by_telegram_id(self, db: AsyncSession, telegram_id: int) -> Optional[User]:
- return await get_user_by_telegram_id(db, telegram_id)
-
- async def add_balance(self, db: AsyncSession, user_id: int, amount_kopeks: int) -> bool:
- return await add_user_balance_by_id(db, user_id, amount_kopeks)
diff --git a/app/external/tribute.py b/app/external/tribute.py
index e731a845..c26c074a 100644
--- a/app/external/tribute.py
+++ b/app/external/tribute.py
@@ -28,16 +28,10 @@ class TributeService:
return None
try:
- payment_url = f"{self.donate_link}?user_id={user_id}"
- if amount_kopeks > 0:
- amount_rubles = amount_kopeks / 100
- payment_url += f"&amount={amount_rubles:.2f}"
+ payment_url = f"{self.donate_link}&user_id={user_id}"
- if description:
- payment_url += f"&description={description}"
-
- logger.info(f"Создана ссылка Tribute для пользователя {user_id}: {amount_kopeks} коп.")
+ logger.info(f"Создана ссылка Tribute для пользователя {user_id}")
return payment_url
except Exception as e:
@@ -45,28 +39,19 @@ class TributeService:
return None
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
- """Проверяет подпись webhook"""
if not self.webhook_secret:
- logger.warning("Webhook secret не настроен, пропускаем проверку подписи")
+ logger.warning("Webhook secret не настроен")
return True
try:
- if signature.startswith('sha256='):
- signature = signature[7:]
-
expected_signature = hmac.new(
- self.webhook_secret.encode('utf-8'),
- payload.encode('utf-8'),
+ self.webhook_secret.encode(),
+ payload.encode(),
hashlib.sha256
).hexdigest()
- result = hmac.compare_digest(signature, expected_signature)
-
- if not result:
- logger.warning(f"Неверная подпись webhook. Получено: {signature[:10]}..., ожидалось: {expected_signature[:10]}...")
-
- return result
+ return hmac.compare_digest(signature, expected_signature)
except Exception as e:
logger.error(f"Ошибка проверки подписи webhook: {e}")
@@ -75,104 +60,49 @@ class TributeService:
async def process_webhook(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
try:
- logger.debug(f"Обработка Tribute webhook: {json.dumps(payload, ensure_ascii=False, indent=2)}")
- payment_id = None
- status = None
- amount_kopeks = 0
- telegram_user_id = None
-
-
- payment_id = payload.get("id") or payload.get("payment_id") or payload.get("donation_id")
+ payment_id = payload.get("id") or payload.get("payment_id")
status = payload.get("status")
- amount_kopeks = payload.get("amount", 0)
+ amount_kopeks = payload.get("amount", 0)
telegram_user_id = payload.get("telegram_user_id") or payload.get("user_id")
if not payment_id and "payload" in payload:
data = payload["payload"]
- payment_id = data.get("id") or data.get("payment_id") or data.get("donation_id")
+ payment_id = data.get("id") or data.get("payment_id")
status = data.get("status")
- amount_kopeks = data.get("amount", 0)
+ amount_kopeks = data.get("amount", 0)
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
if not payment_id and "name" in payload:
event_name = payload.get("name")
data = payload.get("payload", {})
-
- payment_id = (
- data.get("donation_request_id") or
- data.get("donation_id") or
- data.get("id") or
- data.get("payment_id")
- )
-
- amount_kopeks = data.get("amount", 0)
- telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
+ payment_id = str(data.get("donation_request_id"))
+ amount_kopeks = data.get("amount", 0)
+ telegram_user_id = data.get("telegram_user_id")
if event_name == "new_donation":
status = "paid"
- elif event_name == "donation_completed":
- status = "completed"
elif event_name == "cancelled_subscription":
status = "cancelled"
else:
status = "unknown"
- logger.warning(f"Неизвестное событие Tribute: {event_name}")
- if not payment_id and "data" in payload:
- data = payload["data"]
- payment_id = data.get("id") or data.get("donation_id")
- status = data.get("status", "paid")
- amount_kopeks = data.get("amount", 0)
- telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
-
- if isinstance(amount_kopeks, (int, float)):
- if amount_kopeks > 1000:
- amount_kopeks = int(amount_kopeks)
- else:
- amount_kopeks = int(amount_kopeks * 100)
- else:
- amount_kopeks = 0
-
- logger.info(f"Извлеченные данные Tribute webhook:")
- logger.info(f" - payment_id: {payment_id}")
- logger.info(f" - status: {status}")
- logger.info(f" - amount_kopeks: {amount_kopeks}")
- logger.info(f" - telegram_user_id: {telegram_user_id}")
+ logger.info(f"Обработка Tribute webhook: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}")
if not telegram_user_id:
logger.error("Не найден telegram_user_id в webhook данных")
- logger.error(f"Полные данные webhook: {json.dumps(payload, ensure_ascii=False)}")
return None
- if amount_kopeks <= 0:
- logger.error(f"Неверная сумма платежа: {amount_kopeks}")
- return None
-
- if not payment_id:
- import time
- payment_id = f"tribute_{telegram_user_id}_{int(time.time())}"
- logger.info(f"Сгенерирован payment_id: {payment_id}")
- else:
- payment_id = str(payment_id)
-
- if not status:
- status = "paid"
-
- result = {
+ return {
"event_type": "payment",
- "payment_id": payment_id,
+ "payment_id": payment_id or f"tribute_{telegram_user_id}_{amount_kopeks}",
"user_id": int(telegram_user_id),
"amount_kopeks": amount_kopeks,
- "status": status,
- "external_id": f"tribute_{payment_id}",
- "provider": "tribute"
+ "status": status or "paid",
+ "external_id": f"donation_{payment_id}"
}
- logger.info(f"✅ Успешно обработан Tribute webhook: {result}")
- return result
-
except Exception as e:
- logger.error(f"❌ Ошибка обработки Tribute webhook: {e}", exc_info=True)
+ logger.error(f"Ошибка обработки Tribute webhook: {e}")
logger.error(f"Webhook payload: {json.dumps(payload, ensure_ascii=False)}")
return None
diff --git a/app/external/webhook_server.py b/app/external/webhook_server.py
index 0cc76474..417473f6 100644
--- a/app/external/webhook_server.py
+++ b/app/external/webhook_server.py
@@ -1,247 +1,98 @@
import logging
-import asyncio
-from typing import Optional, List
+from typing import Optional
+
from aiohttp import web
from aiogram import Bot
from app.config import settings
-from app.external.tribute import TributeService
-from app.services.payment_service import PaymentService
-from app.external.yookassa_webhook import YooKassaWebhookHandler
+from app.services.tribute_service import TributeService
logger = logging.getLogger(__name__)
class WebhookServer:
- def __init__(self, bot: Bot, payment_service: PaymentService):
+ def __init__(self, bot: Bot):
self.bot = bot
- self.payment_service = payment_service
- self.tribute_service = TributeService()
- self.yookassa_handler = YooKassaWebhookHandler(payment_service)
-
- self.tribute_app = None
- self.tribute_runner = None
- self.tribute_site = None
-
- self.yookassa_app = None
- self.yookassa_runner = None
- self.yookassa_site = None
+ self.app = None
+ self.runner = None
+ self.site = None
+ self.tribute_service = TributeService(bot)
- def _create_logging_middleware(self):
- @web.middleware
- async def logging_middleware(request, handler):
- start_time = request.loop.time()
-
- try:
- response = await handler(request)
- process_time = request.loop.time() - start_time
-
- logger.info(f"{request.method} {request.path_qs} -> {response.status} ({process_time:.3f}s)")
-
- return response
-
- except Exception as e:
- process_time = request.loop.time() - start_time
- logger.error(f"{request.method} {request.path_qs} -> ERROR ({process_time:.3f}s): {e}")
- raise
+ async def create_app(self) -> web.Application:
- return logging_middleware
+ self.app = web.Application()
+
+ self.app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
+ self.app.router.add_get('/health', self._health_check)
+
+ logger.info(f"Webhook сервер настроен:")
+ logger.info(f" - Tribute webhook: {settings.TRIBUTE_WEBHOOK_PATH}")
+ logger.info(f" - Health check: /health")
+
+ return self.app
- async def create_tribute_app(self) -> web.Application:
- self.tribute_app = web.Application()
-
- self.tribute_app.middlewares.append(self._create_logging_middleware())
-
- self.tribute_app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
- self.tribute_app.router.add_get('/health', self._tribute_health_check)
-
- logger.info(f"Tribute webhook настроен:")
- logger.info(f" - Webhook path: {settings.TRIBUTE_WEBHOOK_PATH}")
- logger.info(f" - Port: {settings.TRIBUTE_WEBHOOK_PORT}")
-
- return self.tribute_app
-
- async def create_yookassa_app(self) -> web.Application:
- self.yookassa_app = web.Application()
-
- self.yookassa_app.middlewares.append(self._create_logging_middleware())
-
- self.yookassa_app.router.add_post(settings.YOOKASSA_WEBHOOK_PATH, self.yookassa_handler.handle_webhook)
- self.yookassa_app.router.add_get('/health', self._yookassa_health_check)
-
- logger.info(f"YooKassa webhook настроен:")
- logger.info(f" - Webhook path: {settings.YOOKASSA_WEBHOOK_PATH}")
- logger.info(f" - Port: {settings.YOOKASSA_WEBHOOK_PORT}")
-
- return self.yookassa_app
-
- async def start_tribute_server(self):
- if not settings.TRIBUTE_ENABLED:
- logger.info("Tribute отключен, сервер не запускается")
- return
+ async def start(self):
try:
- await self.create_tribute_app()
+ if not self.app:
+ await self.create_app()
- self.tribute_runner = web.AppRunner(self.tribute_app)
- await self.tribute_runner.setup()
+ self.runner = web.AppRunner(self.app)
+ await self.runner.setup()
- self.tribute_site = web.TCPSite(
- self.tribute_runner,
- host='0.0.0.0',
+ self.site = web.TCPSite(
+ self.runner,
+ host='0.0.0.0',
port=settings.TRIBUTE_WEBHOOK_PORT
)
- await self.tribute_site.start()
+ await self.site.start()
- logger.info(f"✅ Tribute webhook сервер запущен на 0.0.0.0:{settings.TRIBUTE_WEBHOOK_PORT}")
+ logger.info(f"✅ Webhook сервер запущен на порту {settings.TRIBUTE_WEBHOOK_PORT}")
logger.info(f"🎯 Tribute webhook URL: http://your-server:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
except Exception as e:
- logger.error(f"❌ Ошибка запуска Tribute webhook сервера: {e}")
+ logger.error(f"❌ Ошибка запуска webhook сервера: {e}")
raise
- async def start_yookassa_server(self):
- if not settings.is_yookassa_enabled():
- logger.info("YooKassa отключен, сервер не запускается")
- return
-
- try:
- await self.create_yookassa_app()
-
- self.yookassa_runner = web.AppRunner(self.yookassa_app)
- await self.yookassa_runner.setup()
-
- self.yookassa_site = web.TCPSite(
- self.yookassa_runner,
- host='0.0.0.0',
- port=settings.YOOKASSA_WEBHOOK_PORT
- )
-
- await self.yookassa_site.start()
-
- logger.info(f"✅ YooKassa webhook сервер запущен на 0.0.0.0:{settings.YOOKASSA_WEBHOOK_PORT}")
- logger.info(f"🎯 YooKassa webhook URL: http://your-server:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
-
- except Exception as e:
- logger.error(f"❌ Ошибка запуска YooKassa webhook сервера: {e}")
- raise
-
- async def start(self):
- tasks = []
-
- if settings.TRIBUTE_ENABLED:
- tasks.append(self.start_tribute_server())
-
- if settings.is_yookassa_enabled():
- tasks.append(self.start_yookassa_server())
-
- if not tasks:
- logger.warning("Ни один платежный провайдер не включен!")
- return
-
- await asyncio.gather(*tasks)
-
- logger.info("🚀 Все webhook серверы запущены!")
-
async def stop(self):
- tasks = []
- if self.tribute_site:
- tasks.append(self.tribute_site.stop())
- if self.tribute_runner:
- tasks.append(self.tribute_runner.cleanup())
-
- if self.yookassa_site:
- tasks.append(self.yookassa_site.stop())
- if self.yookassa_runner:
- tasks.append(self.yookassa_runner.cleanup())
-
- if tasks:
- try:
- await asyncio.gather(*tasks, return_exceptions=True)
- logger.info("🛑 Все webhook серверы остановлены")
- except Exception as e:
- logger.error(f"Ошибка остановки webhook серверов: {e}")
+ try:
+ if self.site:
+ await self.site.stop()
+ logger.info("Webhook сайт остановлен")
+
+ if self.runner:
+ await self.runner.cleanup()
+ logger.info("Webhook runner очищен")
+
+ except Exception as e:
+ logger.error(f"Ошибка остановки webhook сервера: {e}")
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
+
try:
raw_body = await request.read()
-
- if not raw_body:
- logger.warning("Получен пустой Tribute webhook")
- return web.json_response(
- {"status": "error", "reason": "empty_body"},
- status=400
- )
-
- try:
- payload = raw_body.decode('utf-8')
- import json
- webhook_data = json.loads(payload)
- except (UnicodeDecodeError, json.JSONDecodeError) as e:
- logger.error(f"Ошибка декодирования Tribute webhook: {e}")
- return web.json_response(
- {"status": "error", "reason": "invalid_payload"},
- status=400
- )
+ payload = raw_body.decode('utf-8')
signature = request.headers.get('X-Tribute-Signature')
- if signature and not self.tribute_service.verify_webhook_signature(payload, signature):
- logger.error("Неверная подпись Tribute webhook")
- return web.json_response(
- {"status": "error", "reason": "invalid_signature"},
- status=400
- )
- result = await self.tribute_service.process_webhook(webhook_data)
+ result = await self.tribute_service.process_webhook(payload, signature)
- if result:
- from app.database.database import get_db
- async with get_db() as db:
- success = await self.payment_service.process_tribute_payment(
- db=db,
- user_id=result['user_id'],
- amount_kopeks=result['amount_kopeks'],
- payment_id=result['payment_id']
- )
-
- if success:
- logger.info(f"✅ Успешно обработан Tribute платеж: {result['payment_id']}")
- return web.json_response({"status": "ok"}, status=200)
- else:
- logger.error(f"❌ Ошибка обработки Tribute платежа: {result['payment_id']}")
- return web.json_response(
- {"status": "error", "reason": "processing_failed"},
- status=500
- )
- else:
- logger.error("Не удалось обработать Tribute webhook данные")
- return web.json_response(
- {"status": "error", "reason": "invalid_data"},
- status=400
- )
+ return web.json_response(result, status=200)
except Exception as e:
- logger.error(f"Критическая ошибка обработки Tribute webhook: {e}", exc_info=True)
+ logger.error(f"Ошибка обработки Tribute webhook: {e}")
return web.json_response(
- {"status": "error", "reason": "internal_error"},
+ {"status": "error", "reason": "internal_error"},
status=500
)
- async def _tribute_health_check(self, request: web.Request) -> web.Response:
+ async def _health_check(self, request: web.Request) -> web.Response:
+
return web.json_response({
"status": "ok",
"service": "tribute-webhooks",
- "tribute_enabled": settings.TRIBUTE_ENABLED,
- "port": settings.TRIBUTE_WEBHOOK_PORT
- })
-
- async def _yookassa_health_check(self, request: web.Request) -> web.Response:
- return web.json_response({
- "status": "ok",
- "service": "yookassa-webhooks",
- "yookassa_enabled": settings.is_yookassa_enabled(),
- "port": settings.YOOKASSA_WEBHOOK_PORT
- })
+ "tribute_enabled": settings.TRIBUTE_ENABLED
+ })
\ No newline at end of file
diff --git a/app/handlers/balance.py b/app/handlers/balance.py
index d3ee3a96..033b5629 100644
--- a/app/handlers/balance.py
+++ b/app/handlers/balance.py
@@ -500,7 +500,7 @@ async def check_yookassa_payment_status(
emoji = status_emoji.get(payment.status, "❓")
status = status_text.get(payment.status, "Неизвестно")
- message_text = (f"💳 Статус платежа\n\n"
+ message_text = (f"💳 Статус платежа\n\n"
f"🆔 ID: {payment.yookassa_payment_id[:8]}...\n"
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n"
f"📊 Статус: {emoji} {status}\n"
diff --git a/app/localization/texts.py b/app/localization/texts.py
index 52142845..9a540250 100644
--- a/app/localization/texts.py
+++ b/app/localization/texts.py
@@ -342,26 +342,6 @@ class RussianTexts(Texts):
💳 Требуется: {required}
Пополните баланс и продлите подписку вручную.
-"""
-
- PAYMENT_SUCCESS_TRIBUTE = """
-✅ Пополнение выполнено!
-
-💰 Сумма: {amount} ₽
-🎯 Способ: Tribute
-🆔 ID платежа: {payment_id}
-
-Средства зачислены на ваш баланс!
-"""
-
- PAYMENT_SUCCESS_YOOKASSA = """
-✅ Пополнение выполнено!
-
-💰 Сумма: {amount} ₽
-🏦 Способ: Банковская карта (YooKassa)
-🆔 ID платежа: {payment_id}
-
-Средства зачислены на ваш баланс!
"""
SUPPORT_INFO = f"""
@@ -411,6 +391,29 @@ class RussianTexts(Texts):
Не забудьте продлить подписку, чтобы не потерять доступ к серверам.
"""
+
+ SUBSCRIPTION_EXPIRED = """
+❌ Подписка истекла
+
+Ваша подписка истекла. Для восстановления доступа продлите подписку.
+"""
+
+ AUTOPAY_SUCCESS = """
+✅ Автоплатеж выполнен
+
+Ваша подписка автоматически продлена на {days} дней.
+Списано с баланса: {amount}
+"""
+
+ AUTOPAY_FAILED = """
+❌ Ошибка автоплатежа
+
+Не удалось списать средства для продления подписки.
+Недостаточно средств на балансе: {balance}
+Требуется: {required}
+
+Пополните баланс и продлите подписку вручную.
+"""
class EnglishTexts(Texts):
@@ -504,20 +507,3 @@ def clear_rules_cache():
global _cached_rules
_cached_rules.clear()
print("✅ Кеш правил очищен")
-
-def get_text(key: str, language: str = "ru") -> str:
- texts = get_texts(language)
-
- text_mapping = {
- "payment_success_tribute": getattr(texts, "PAYMENT_SUCCESS_TRIBUTE",
- "✅ Пополнение через Tribute выполнено! Сумма: {amount} ₽, ID: {payment_id}"),
- "payment_success_yookassa": getattr(texts, "PAYMENT_SUCCESS_YOOKASSA",
- "✅ Пополнение через YooKassa выполнено! Сумма: {amount} ₽, ID: {payment_id}"),
- "welcome": texts.WELCOME,
- "back": texts.BACK,
- "cancel": texts.CANCEL,
- "confirm": texts.CONFIRM,
- "continue": texts.CONTINUE,
- }
-
- return text_mapping.get(key, f"Текст для ключа '{key}' не найден")
diff --git a/app/services/maintenance_service.py b/app/services/maintenance_service.py
index 1621807a..d6615409 100644
--- a/app/services/maintenance_service.py
+++ b/app/services/maintenance_service.py
@@ -35,14 +35,12 @@ class MaintenanceService:
return self._status
def is_maintenance_active(self) -> bool:
- """Проверяет, активен ли режим техработ"""
return self._status.is_active
def get_maintenance_message(self) -> str:
- """Получает сообщение о техработах"""
if self._status.auto_enabled:
return f"""
-🔧 Технические работы
+🔧 Технические работы!
Сервис временно недоступен из-за проблем с подключением к серверам.
@@ -67,10 +65,6 @@ class MaintenanceService:
await self._save_status_to_cache()
logger.warning(f"🔧 Режим техработ ВКЛЮЧЕН. Причина: {self._status.reason}")
-
- if auto:
- await self._notify_admins_maintenance_enabled(reason)
-
return True
except Exception as e:
@@ -83,8 +77,6 @@ class MaintenanceService:
logger.info("Режим техработ уже выключен")
return True
- was_auto_enabled = self._status.auto_enabled
-
self._status.is_active = False
self._status.enabled_at = None
self._status.reason = None
@@ -94,10 +86,6 @@ class MaintenanceService:
await self._save_status_to_cache()
logger.info("✅ Режим техработ ВЫКЛЮЧЕН")
-
- if was_auto_enabled:
- await self._notify_admins_maintenance_disabled()
-
return True
except Exception as e:
@@ -105,7 +93,6 @@ class MaintenanceService:
return False
async def start_monitoring(self) -> bool:
- """Запускает мониторинг API RemnaWave"""
try:
if self._check_task and not self._check_task.done():
logger.warning("Мониторинг уже запущен")
@@ -194,10 +181,9 @@ class MaintenanceService:
break
except Exception as e:
logger.error(f"Ошибка в цикле мониторинга: {e}")
- await asyncio.sleep(30)
+ await asyncio.sleep(30)
async def _save_status_to_cache(self):
- """Сохраняет состояние в кеше"""
try:
status_data = {
"is_active": self._status.is_active,
@@ -278,84 +264,5 @@ class MaintenanceService:
"consecutive_failures": self._status.consecutive_failures
}
- def set_bot(self, bot):
- self._bot = bot
-
- async def _notify_admins_maintenance_enabled(self, reason: Optional[str] = None):
- if not hasattr(self, '_bot') or not self._bot:
- return
-
- try:
- from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
-
- admin_ids = settings.get_admin_ids()
- if not admin_ids:
- return
-
- enabled_time = self._status.enabled_at.strftime("%d.%m.%Y %H:%M:%S") if self._status.enabled_at else "неизвестно"
-
- message = f"""
- 🔧 Режим техработ автоматически ВКЛЮЧЕН
-
- ⏰ Время: {enabled_time}
- 📝 Причина: {reason or 'Недоступность API'}
- 🔄 Неудачных проверок: {self._status.consecutive_failures}
-
- ℹ️ Обычные пользователи заблокированы до восстановления API.
- ⚙️ Админы имеют полный доступ к боту.
-
- 🔍 Для управления используйте админ-панель → Техработы
- """
-
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
- [InlineKeyboardButton(text="🔧 Панель техработ", callback_data="maintenance_panel")]
- ])
-
- for admin_id in admin_ids:
- try:
- await self._bot.send_message(
- admin_id,
- message,
- parse_mode="HTML",
- reply_markup=keyboard
- )
- except Exception as e:
- logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
-
- except Exception as e:
- logger.error(f"Ошибка отправки уведомлений админам о включении техработ: {e}")
-
- async def _notify_admins_maintenance_disabled(self):
- if not hasattr(self, '_bot') or not self._bot:
- return
-
- try:
- admin_ids = settings.get_admin_ids()
- if not admin_ids:
- return
-
- disabled_time = datetime.utcnow().strftime("%d.%m.%Y %H:%M:%S")
-
- message = f"""
- ✅ Режим техработ автоматически ОТКЛЮЧЕН
-
- ⏰ Время: {disabled_time}
- 🔄 API восстановлено: Подключение к Remnawave работает
-
- ℹ️ Пользователи снова могут использовать бота.
- """
-
- for admin_id in admin_ids:
- try:
- await self._bot.send_message(
- admin_id,
- message,
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
-
- except Exception as e:
- logger.error(f"Ошибка отправки уведомлений админам о выключении техработ: {e}")
maintenance_service = MaintenanceService()
diff --git a/app/services/payment_service.py b/app/services/payment_service.py
index 8580e976..84efda4d 100644
--- a/app/services/payment_service.py
+++ b/app/services/payment_service.py
@@ -131,81 +131,6 @@ class PaymentService:
except Exception as e:
logger.error(f"Ошибка создания платежа YooKassa: {e}")
return None
-
- async def process_tribute_payment(
- self,
- db: AsyncSession,
- user_id: int,
- amount_kopeks: int,
- payment_id: str
- ) -> bool:
-
- try:
- logger.info(f"Обработка Tribute платежа: telegram_id={user_id}, amount={amount_kopeks} коп., payment_id={payment_id}")
-
- from app.database.crud.user import UserCRUD
- user_crud = UserCRUD()
-
- user = await user_crud.get_user_by_telegram_id(db, user_id)
- if not user:
- logger.error(f"Пользователь с telegram_id {user_id} не найден для обработки Tribute платежа")
- return False
-
- from app.database.crud.transaction import TransactionCRUD
- transaction_crud = TransactionCRUD()
-
- existing_transaction = await transaction_crud.get_transaction_by_external_id(
- db, f"tribute_{payment_id}"
- )
-
- if existing_transaction:
- logger.info(f"Платеж {payment_id} уже был обработан ранее")
- return True
-
- transaction_data = {
- "user_id": user.id,
- "amount_kopeks": amount_kopeks,
- "transaction_type": "deposit",
- "status": "completed",
- "external_id": f"tribute_{payment_id}",
- "payment_system": "tribute",
- "description": f"Пополнение через Tribute: {payment_id}"
- }
-
- transaction = await transaction_crud.create_transaction(db, transaction_data)
-
- if not transaction:
- logger.error(f"Не удалось создать транзакцию для Tribute платежа {payment_id}")
- return False
-
- success = await user_crud.add_balance(db, user.id, amount_kopeks)
-
- if not success:
- logger.error(f"Не удалось пополнить баланс пользователя {user.id}")
- await transaction_crud.update_transaction_status(db, transaction.id, "failed")
- return False
-
- try:
- from app.localization.texts import get_text
-
- amount_rubles = amount_kopeks / 100
- message = get_text("payment_success_tribute").format(
- amount=f"{amount_rubles:.2f}",
- payment_id=payment_id
- )
-
- if hasattr(self, 'bot') and self.bot:
- await self.bot.send_message(user.telegram_id, message)
-
- except Exception as e:
- logger.warning(f"Не удалось отправить уведомление о платеже пользователю {user.telegram_id}: {e}")
-
- logger.info(f"✅ Успешно обработан Tribute платеж {payment_id} для пользователя {user.telegram_id}")
- return True
-
- except Exception as e:
- logger.error(f"❌ Ошибка обработки Tribute платежа {payment_id}: {e}", exc_info=True)
- return False
async def process_yookassa_webhook(self, db: AsyncSession, webhook_data: dict) -> bool:
try:
diff --git a/app/services/referral_service.py b/app/services/referral_service.py
index a68ed10d..1b22f4f3 100644
--- a/app/services/referral_service.py
+++ b/app/services/referral_service.py
@@ -20,7 +20,7 @@ async def process_referral_registration(
referrer = await get_user_by_id(db, referrer_id)
if not new_user or not referrer:
- logger.error(f"Пользователи не найдены: new_user_id={new_user_id}, referrer_id={referrer_id}")
+ logger.error(f"Пользователи не найдены: {new_user_id}, {referrer_id}")
return False
if new_user.referred_by_id != referrer_id:
@@ -81,13 +81,9 @@ async def process_referral_purchase(
logger.info(f"🔍 Покупка реферала {user_id}: первая = {is_first_purchase}, сумма = {purchase_amount_kopeks/100}₽")
- if is_first_purchase and settings.REFERRAL_REGISTRATION_REWARD > 0:
+ if is_first_purchase:
reward_amount = settings.REFERRAL_REGISTRATION_REWARD
- if reward_amount > 1000000:
- logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: reward_amount = {reward_amount} слишком большой! Проверьте настройки REFERRAL_REGISTRATION_REWARD")
- reward_amount = 10000
-
await add_user_balance(
db, referrer, reward_amount,
f"Реферальная награда за первую покупку {user.full_name}"
@@ -104,18 +100,12 @@ async def process_referral_purchase(
logger.info(f"🎉 Первая покупка реферала: {referrer.telegram_id} получил {reward_amount/100}₽")
- if not (0 <= settings.REFERRAL_COMMISSION_PERCENT <= 100):
- logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: REFERRAL_COMMISSION_PERCENT = {settings.REFERRAL_COMMISSION_PERCENT} некорректный! Должен быть от 0 до 100")
- commission_percent = 10
- else:
- commission_percent = settings.REFERRAL_COMMISSION_PERCENT
-
- commission_amount = int(purchase_amount_kopeks * commission_percent / 100)
+ commission_amount = int(purchase_amount_kopeks * settings.REFERRAL_COMMISSION_PERCENT / 100)
if commission_amount > 0:
await add_user_balance(
db, referrer, commission_amount,
- f"Комиссия {commission_percent}% с покупки {user.full_name}"
+ f"Комиссия {settings.REFERRAL_COMMISSION_PERCENT}% с покупки {user.full_name}"
)
await create_referral_earning(
@@ -138,8 +128,6 @@ async def process_referral_purchase(
except Exception as e:
logger.error(f"Ошибка обработки покупки реферала: {e}")
- import traceback
- logger.error(f"Полный traceback: {traceback.format_exc()}")
return False
@@ -153,7 +141,7 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
invited_count_result = await db.execute(
select(func.count(User.id)).where(User.referred_by_id == user_id)
)
- invited_count = invited_count_result.scalar() or 0
+ invited_count = invited_count_result.scalar()
paid_referrals_result = await db.execute(
select(func.count(User.id)).where(
@@ -161,13 +149,13 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
User.has_had_paid_subscription == True
)
)
- paid_referrals_count = paid_referrals_result.scalar() or 0
+ paid_referrals_count = paid_referrals_result.scalar()
- total_earned = await get_referral_earnings_sum(db, user_id) or 0
+ total_earned = await get_referral_earnings_sum(db, user_id)
from datetime import datetime, timedelta
month_ago = datetime.utcnow() - timedelta(days=30)
- month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago) or 0
+ month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago)
return {
"invited_count": invited_count,
@@ -183,4 +171,4 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
"paid_referrals_count": 0,
"total_earned_kopeks": 0,
"month_earned_kopeks": 0
- }
+ }
\ No newline at end of file
diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py
index a5acd8cc..e9c3a521 100644
--- a/app/services/subscription_service.py
+++ b/app/services/subscription_service.py
@@ -306,109 +306,43 @@ class SubscriptionService:
try:
needs_cleanup = False
- if not isinstance(subscription.connected_squads, list):
- logger.warning(f"Исправляем connected_squads для пользователя {user.telegram_id}")
- subscription.connected_squads = []
- needs_cleanup = True
-
- if subscription.connected_squads:
- unique_squads = list(set([squad for squad in subscription.connected_squads if squad and isinstance(squad, str)]))
- if len(unique_squads) != len(subscription.connected_squads):
- logger.info(f"Очищены дубликаты в connected_squads для пользователя {user.telegram_id}")
- subscription.connected_squads = unique_squads
- needs_cleanup = True
-
- if subscription.traffic_limit_gb < 0:
- logger.warning(f"Отрицательный traffic_limit_gb исправлен на 0 для пользователя {user.telegram_id}")
- subscription.traffic_limit_gb = 0
- needs_cleanup = True
-
- if subscription.traffic_used_gb < 0:
- logger.warning(f"Отрицательный traffic_used_gb исправлен на 0 для пользователя {user.telegram_id}")
- subscription.traffic_used_gb = 0.0
- needs_cleanup = True
-
- if subscription.device_limit < 1:
- logger.warning(f"device_limit < 1 исправлен на 1 для пользователя {user.telegram_id}")
- subscription.device_limit = 1
- needs_cleanup = True
- elif subscription.device_limit > 10:
- logger.warning(f"device_limit > 10 исправлен на 10 для пользователя {user.telegram_id}")
- subscription.device_limit = 10
- needs_cleanup = True
-
- from datetime import datetime
- current_time = datetime.utcnow()
-
- if subscription.start_date > current_time + timedelta(days=1):
- logger.warning(f"Некорректная start_date исправлена для пользователя {user.telegram_id}")
- subscription.start_date = current_time
- needs_cleanup = True
-
- if subscription.end_date < subscription.start_date:
- logger.warning(f"end_date раньше start_date исправлено для пользователя {user.telegram_id}")
- subscription.end_date = subscription.start_date + timedelta(days=1)
- needs_cleanup = True
-
if user.remnawave_uuid:
try:
async with self.api as api:
remnawave_user = await api.get_user_by_uuid(user.remnawave_uuid)
if not remnawave_user:
- logger.warning(f"Пользователь {user.telegram_id} имеет UUID {user.remnawave_uuid}, но не найден в панели")
+ logger.warning(f"⚠️ Пользователь {user.telegram_id} имеет UUID {user.remnawave_uuid}, но не найден в панели")
needs_cleanup = True
else:
if remnawave_user.telegram_id != user.telegram_id:
- logger.warning(f"Несоответствие telegram_id для пользователя {user.telegram_id}")
+ logger.warning(f"⚠️ Несоответствие telegram_id для пользователя {user.telegram_id}")
needs_cleanup = True
-
- if remnawave_user.subscription_url and not subscription.subscription_url:
- subscription.subscription_url = remnawave_user.subscription_url
- logger.info(f"Восстановлена subscription_url из панели для пользователя {user.telegram_id}")
- needs_cleanup = True
-
except Exception as api_error:
- logger.error(f"Ошибка проверки пользователя в панели: {api_error}")
+ logger.error(f"❌ Ошибка проверки пользователя в панели: {api_error}")
needs_cleanup = True
if subscription.remnawave_short_uuid and not user.remnawave_uuid:
- logger.warning(f"У подписки есть short_uuid, но у пользователя нет remnawave_uuid")
+ logger.warning(f"⚠️ У подписки есть short_uuid, но у пользователя нет remnawave_uuid")
needs_cleanup = True
-
- if subscription.subscription_url:
- if not subscription.subscription_url.startswith(('http://', 'https://')):
- logger.warning(f"Некорректный subscription_url для пользователя {user.telegram_id}")
- subscription.subscription_url = ""
- needs_cleanup = True
-
- if needs_cleanup and (
- not user.remnawave_uuid or
- subscription.remnawave_short_uuid and not user.remnawave_uuid
- ):
- logger.info(f"Очищаем критические мусорные данные подписки для пользователя {user.telegram_id}")
+
+ if needs_cleanup:
+ logger.info(f"🧹 Очищаем мусорные данные подписки для пользователя {user.telegram_id}")
subscription.remnawave_short_uuid = None
subscription.subscription_url = ""
-
+ subscription.connected_squads = []
user.remnawave_uuid = None
- if needs_cleanup:
- subscription.updated_at = current_time
await db.commit()
- logger.info(f"Исправления применены для пользователя {user.telegram_id}")
+ logger.info(f"✅ Мусорные данные очищены для пользователя {user.telegram_id}")
return True
except Exception as e:
- logger.error(f"Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
- import traceback
- logger.error(f"Полный traceback: {traceback.format_exc()}")
- try:
- await db.rollback()
- except:
- pass
+ logger.error(f"❌ Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
+ await db.rollback()
return False
async def get_countries_price_by_uuids(