"""Фоновый сервис для обработки очереди чеков NaloGO. При временной недоступности сервиса nalog.ru (503), чеки сохраняются в Redis и отправляются позже этим сервисом. """ import asyncio import logging from datetime import datetime, timedelta from typing import Optional from dateutil.parser import isoparse from aiogram import Bot from app.config import settings from app.services.nalogo_service import NaloGoService from app.utils.cache import cache logger = logging.getLogger(__name__) class NalogoQueueService: """Сервис фоновой обработки очереди чеков NaloGO.""" def __init__(self, nalogo_service: Optional[NaloGoService] = None): self._nalogo_service = nalogo_service self._bot: Optional[Bot] = None self._task: Optional[asyncio.Task] = None self._running = False self._last_notification_time: Optional[datetime] = None self._notification_cooldown = timedelta(hours=1) # Не чаще раза в час self._had_pending_receipts = False # Флаг для отслеживания успешной разгрузки def set_nalogo_service(self, service: NaloGoService) -> None: """Установить сервис NaloGO.""" self._nalogo_service = service def set_bot(self, bot: Bot) -> None: """Установить бота для отправки уведомлений.""" self._bot = bot def is_running(self) -> bool: """Проверка, запущен ли сервис.""" return self._running and self._task is not None and not self._task.done() @property def _check_interval(self) -> int: """Интервал проверки очереди в секундах.""" return getattr(settings, "NALOGO_QUEUE_CHECK_INTERVAL", 300) @property def _receipt_delay(self) -> int: """Задержка между отправкой чеков в секундах.""" return getattr(settings, "NALOGO_QUEUE_RECEIPT_DELAY", 3) @property def _max_attempts(self) -> int: """Максимальное количество попыток отправки чека.""" return getattr(settings, "NALOGO_QUEUE_MAX_ATTEMPTS", 10) async def start(self) -> None: """Запустить фоновую обработку очереди.""" if not self._nalogo_service or not self._nalogo_service.configured: logger.info("NaloGO не настроен, сервис очереди чеков не запущен") return if self.is_running(): logger.warning("Сервис очереди чеков уже запущен") return self._running = True self._task = asyncio.create_task(self._process_queue_loop()) logger.info( f"Сервис очереди чеков NaloGO запущен " f"(интервал: {self._check_interval}с, задержка между чеками: {self._receipt_delay}с)" ) async def stop(self) -> None: """Остановить фоновую обработку.""" self._running = False if self._task and not self._task.done(): self._task.cancel() try: await self._task except asyncio.CancelledError: pass self._task = None logger.info("Сервис очереди чеков NaloGO остановлен") async def _send_admin_notification(self, message: str, skip_cooldown: bool = False) -> None: """Отправить уведомление админам о чеках.""" if not self._bot: return chat_id = settings.get_admin_notifications_chat_id() if not chat_id: return topic_id = settings.ADMIN_NOTIFICATIONS_NALOG_TOPIC_ID # Проверяем cooldown (можно пропустить для важных уведомлений) if not skip_cooldown: now = datetime.now() if self._last_notification_time: if now - self._last_notification_time < self._notification_cooldown: logger.debug("Уведомление о чеках пропущено (cooldown)") return try: await self._bot.send_message( chat_id=chat_id, message_thread_id=topic_id, text=message, parse_mode="HTML", ) self._last_notification_time = datetime.now() logger.info("Отправлено уведомление о чеках NaloGO") except Exception as error: logger.error(f"Ошибка отправки уведомления о чеках: {error}") async def _process_queue_loop(self) -> None: """Основной цикл обработки очереди.""" while self._running: try: await self._process_pending_receipts() except Exception as error: logger.error(f"Ошибка в цикле обработки очереди чеков: {error}") await asyncio.sleep(self._check_interval) async def _process_pending_receipts(self) -> None: """Обработать все ожидающие чеки в очереди.""" if not self._nalogo_service: return queue_length = await self._nalogo_service.get_queue_length() if queue_length == 0: return logger.info(f"Начинаем обработку очереди чеков: {queue_length} шт.") self._had_pending_receipts = True processed = 0 failed = 0 skipped = 0 total_processed_amount = 0.0 service_unavailable = False while True: receipt_data = await self._nalogo_service.pop_receipt_from_queue() if not receipt_data: break attempts = receipt_data.get("attempts", 0) payment_id = receipt_data.get("payment_id", "unknown") amount = receipt_data.get("amount", 0) # Логируем количество попыток (чек никогда не удаляется из очереди) if attempts >= 10: logger.warning( f"Чек {payment_id} уже {attempts} попыток, продолжаем пытаться..." ) # Пытаемся отправить чек try: # Восстанавливаем описание из сохранённых данных telegram_user_id = receipt_data.get("telegram_user_id") amount_kopeks = receipt_data.get("amount_kopeks") # Извлекаем время оплаты из очереди (чтобы чек был с правильным временем) operation_time = None created_at_str = receipt_data.get("created_at") if created_at_str: try: operation_time = isoparse(created_at_str) except (ValueError, TypeError) as parse_error: logger.warning( f"Не удалось распарсить created_at '{created_at_str}': {parse_error}" ) # Формируем описание заново из настроек (если есть данные) if amount_kopeks is not None: receipt_name = settings.get_balance_payment_description( amount_kopeks, telegram_user_id ) else: # Fallback на сохранённое имя receipt_name = receipt_data.get( "name", settings.get_balance_payment_description(int(amount * 100), telegram_user_id) ) receipt_uuid = await self._nalogo_service.create_receipt( name=receipt_name, amount=amount, quantity=receipt_data.get("quantity", 1), client_info=receipt_data.get("client_info"), payment_id=payment_id, queue_on_failure=False, # Не добавлять в очередь повторно автоматически telegram_user_id=telegram_user_id, amount_kopeks=amount_kopeks, operation_time=operation_time, # Время оплаты, а не отправки ) if receipt_uuid: processed += 1 total_processed_amount += amount # Удаляем метку "в очереди" (чек создан успешно) if payment_id: queued_key = f"nalogo:queued:{payment_id}" await cache.delete(queued_key) logger.info( f"Чек из очереди успешно создан: {receipt_uuid} " f"(payment_id={payment_id}, попытка {attempts + 1})" ) else: # Вернуть в очередь с увеличенным счетчиком попыток await self._nalogo_service.requeue_receipt(receipt_data) failed += 1 service_unavailable = True logger.warning( f"Не удалось создать чек из очереди (payment_id={payment_id}), " f"возвращен в очередь (попытка {attempts + 1}/{self._max_attempts})" ) # Если сервис недоступен, прекращаем попытки до следующего цикла break except Exception as error: await self._nalogo_service.requeue_receipt(receipt_data) failed += 1 logger.error( f"Ошибка при создании чека из очереди (payment_id={payment_id}): {error}" ) # Прекращаем попытки при ошибке break # Задержка между чеками чтобы не долбить API await asyncio.sleep(self._receipt_delay) if processed > 0 or failed > 0 or skipped > 0: logger.info( f"Обработка очереди завершена: " f"успешно={processed}, неудачно={failed}, пропущено={skipped}" ) # Проверяем остаток в очереди remaining = await self._nalogo_service.get_queue_length() # Отправляем уведомление если есть проблемы if service_unavailable or failed > 0: if remaining > 0: queued = await self._nalogo_service.get_queued_receipts() total_queued_amount = sum(r.get("amount", 0) for r in queued) message = ( f"⚠️ Проблема с отправкой чеков NaloGO\n\n" f"Сервис nalog.ru временно недоступен.\n\n" f"📋 В очереди: {remaining} чек(ов)\n" f"💰 На сумму: {total_queued_amount:,.2f} ₽\n\n" f"Чеки будут отправлены автоматически когда сервис восстановится." ) await self._send_admin_notification(message) # Уведомление об успешной разгрузке очереди elif remaining == 0 and self._had_pending_receipts and processed > 0: self._had_pending_receipts = False message = ( f"✅ Очередь чеков NaloGO разгружена\n\n" f"Все отложенные чеки успешно отправлены!\n\n" f"📋 Отправлено: {processed} чек(ов)\n" f"💰 На сумму: {total_processed_amount:,.2f} ₽" ) await self._send_admin_notification(message, skip_cooldown=True) async def force_process(self) -> dict: """Принудительно обработать очередь (для ручного запуска).""" if not self._nalogo_service: return {"error": "NaloGO сервис не настроен"} queue_length = await self._nalogo_service.get_queue_length() if queue_length == 0: return {"message": "Очередь пуста", "processed": 0} await self._process_pending_receipts() new_length = await self._nalogo_service.get_queue_length() return { "message": "Обработка завершена", "was_in_queue": queue_length, "remaining": new_length, "processed": queue_length - new_length, } async def get_status(self) -> dict: """Получить статус сервиса и очереди.""" queue_length = 0 total_amount = 0.0 queued_receipts = [] pending_verification_count = 0 pending_verification_amount = 0.0 pending_verification_receipts = [] if self._nalogo_service: queue_length = await self._nalogo_service.get_queue_length() if queue_length > 0: queued_receipts = await self._nalogo_service.get_queued_receipts() total_amount = sum(r.get("amount", 0) for r in queued_receipts) # Чеки ожидающие ручной проверки pending_verification_count = await self._nalogo_service.get_pending_verification_count() if pending_verification_count > 0: pending_verification_receipts = await self._nalogo_service.get_pending_verification_receipts() pending_verification_amount = sum(r.get("amount", 0) for r in pending_verification_receipts) return { "running": self.is_running(), "check_interval_seconds": self._check_interval, "receipt_delay_seconds": self._receipt_delay, "queue_length": queue_length, "total_amount": total_amount, "max_attempts": self._max_attempts, "queued_receipts": queued_receipts[:10], # Чеки требующие ручной проверки (таймаут после успешной авторизации) "pending_verification_count": pending_verification_count, "pending_verification_amount": pending_verification_amount, "pending_verification_receipts": pending_verification_receipts[:10], } # Глобальный экземпляр сервиса nalogo_queue_service = NalogoQueueService()