diff --git a/app/services/admin_notification_service.py b/app/services/admin_notification_service.py index 1aa3efeb..91b48bd2 100644 --- a/app/services/admin_notification_service.py +++ b/app/services/admin_notification_service.py @@ -50,27 +50,46 @@ class AdminNotificationService: return f"ID {referred_by_id}" async def _get_user_promo_group(self, db: AsyncSession, user: User) -> Optional[PromoGroup]: - if getattr(user, "promo_group", None): - return user.promo_group + existing_promo_group = getattr(user, "promo_group", None) + if existing_promo_group is not None: + return existing_promo_group - if not user.promo_group_id: + promo_group_id = getattr(user, "promo_group_id", None) + if not promo_group_id: return None try: - await db.refresh(user, attribute_names=["promo_group"]) - except Exception: - # relationship might not be available — fallback to direct fetch - pass + return await get_promo_group_by_id(db, promo_group_id) + except RuntimeError as runtime_error: + if "greenlet_spawn" in str(runtime_error): + logger.warning( + "Не удалось загрузить промогруппу через текущую сессию (greenlet_spawn). " + "Попытка повторной загрузки в новой сессии." + ) + try: + from app.database.database import AsyncSessionLocal - if getattr(user, "promo_group", None): - return user.promo_group - - try: - return await get_promo_group_by_id(db, user.promo_group_id) + async with AsyncSessionLocal() as fallback_session: + return await get_promo_group_by_id(fallback_session, promo_group_id) + except Exception as fallback_error: + logger.error( + "Повторная загрузка промогруппы %s пользователя %s не удалась: %s", + promo_group_id, + user.telegram_id, + fallback_error, + ) + return None + logger.error( + "Ошибка загрузки промогруппы %s пользователя %s: %s", + promo_group_id, + user.telegram_id, + runtime_error, + ) + return None except Exception as e: logger.error( "Ошибка загрузки промогруппы %s пользователя %s: %s", - user.promo_group_id, + promo_group_id, user.telegram_id, e, ) @@ -354,29 +373,92 @@ class AdminNotificationService: return False try: - deposit_count_result = await db.execute( - select(func.count()) - .select_from(Transaction) - .where( - Transaction.user_id == user.id, - Transaction.type == TransactionType.DEPOSIT.value, - Transaction.is_completed.is_(True) - ) + message = await self._build_balance_topup_message( + db, + user=user, + transaction=transaction, + old_balance=old_balance, ) - deposit_count = deposit_count_result.scalar_one() or 0 - topup_status = "🆕 Первое пополнение" if deposit_count <= 1 else "🔄 Пополнение" - payment_method = self._get_payment_method_display(transaction.payment_method) - balance_change = user.balance_kopeks - old_balance - referrer_info = await self._get_referrer_info(db, user.referred_by_id) - subscription_result = await db.execute( - select(Subscription).where(Subscription.user_id == user.id) - ) - subscription = subscription_result.scalar_one_or_none() - subscription_status = self._get_subscription_status(subscription) - promo_group = await self._get_user_promo_group(db, user) - promo_block = self._format_promo_group_block(promo_group) + except RuntimeError as runtime_error: + if "greenlet_spawn" not in str(runtime_error): + logger.error(f"Ошибка подготовки уведомления о пополнении: {runtime_error}") + return False - message = f"""💰 ПОПОЛНЕНИЕ БАЛАНСА + logger.warning( + "Повторная подготовка уведомления о пополнении в новой сессии из-за ошибки greenlet_spawn" + ) + try: + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as fallback_session: + fallback_user = await fallback_session.get(User, user.id) or user + fallback_transaction = await fallback_session.get(Transaction, transaction.id) or transaction + message = await self._build_balance_topup_message( + fallback_session, + user=fallback_user, + transaction=fallback_transaction, + old_balance=old_balance, + ) + except Exception as fallback_error: + logger.error( + "Ошибка повторной подготовки уведомления о пополнении: %s", + fallback_error, + exc_info=True, + ) + return False + except Exception as preparation_error: + logger.error( + "Ошибка подготовки уведомления о пополнении: %s", + preparation_error, + exc_info=True, + ) + return False + + if not message: + logger.error("Не удалось сформировать сообщение о пополнении") + return False + + try: + return await self._send_message(message) + except Exception as send_error: + logger.error( + "Ошибка отправки уведомления о пополнении: %s", + send_error, + exc_info=True, + ) + return False + + async def _build_balance_topup_message( + self, + db: AsyncSession, + *, + user: User, + transaction: Transaction, + old_balance: int, + ) -> str: + deposit_count_result = await db.execute( + select(func.count()) + .select_from(Transaction) + .where( + Transaction.user_id == user.id, + Transaction.type == TransactionType.DEPOSIT.value, + Transaction.is_completed.is_(True) + ) + ) + deposit_count = deposit_count_result.scalar_one() or 0 + topup_status = "🆕 Первое пополнение" if deposit_count <= 1 else "🔄 Пополнение" + payment_method = self._get_payment_method_display(transaction.payment_method) + balance_change = user.balance_kopeks - old_balance + referrer_info = await self._get_referrer_info(db, user.referred_by_id) + subscription_result = await db.execute( + select(Subscription).where(Subscription.user_id == user.id) + ) + subscription = subscription_result.scalar_one_or_none() + subscription_status = self._get_subscription_status(subscription) + promo_group = await self._get_user_promo_group(db, user) + promo_block = self._format_promo_group_block(promo_group) + + return f"""💰 ПОПОЛНЕНИЕ БАЛАНСА 👤 Пользователь: {user.full_name} 🆔 Telegram ID: {user.telegram_id} @@ -399,12 +481,6 @@ class AdminNotificationService: 📱 Подписка: {subscription_status} ⏰ {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}""" - - return await self._send_message(message) - - except Exception as e: - logger.error(f"Ошибка отправки уведомления о пополнении: {e}") - return False async def send_subscription_extension_notification( self,