"""Фоновый сервис для обработки очереди чеков NaloGO. При временной недоступности сервиса nalog.ru (503), чеки сохраняются в Redis и отправляются позже этим сервисом. """ import asyncio import logging from datetime import datetime, timedelta from aiogram import Bot from dateutil.parser import isoparse 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: NaloGoService | None = None): self._nalogo_service = nalogo_service self._bot: Bot | None = None self._task: asyncio.Task | None = None self._running = False self._last_notification_time: datetime | None = 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'Обработка очереди завершена: успешно={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()