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(