diff --git a/app/services/payment/common.py b/app/services/payment/common.py index ce819f62..a4a2d3d7 100644 --- a/app/services/payment/common.py +++ b/app/services/payment/common.py @@ -132,6 +132,28 @@ class PaymentCommonMixin: payment_method_title: str | None = None, ) -> None: """Отправляет пользователю уведомление об успешном платеже.""" + # Lazy import to avoid circular dependency + from app.cabinet.routes.websocket import notify_user_balance_topup + + # Send WebSocket notification to cabinet frontend (works for both Telegram and email-only users) + user_id = getattr(user, 'id', None) if user else None + if user_id: + try: + # Get new balance from user + new_balance = getattr(user, 'balance_kopeks', 0) + await notify_user_balance_topup( + user_id=user_id, + amount_kopeks=amount_kopeks, + new_balance_kopeks=new_balance, + description=payment_method_title or '', + ) + except Exception as ws_error: + logger.warning( + 'Не удалось отправить WS уведомление о пополнении баланса для user_id=%s: %s', + user_id, + ws_error, + ) + if not getattr(self, 'bot', None): # Если бот не передан (например, внутри фоновых задач), уведомление пропускаем. return diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py index 573e5a74..64b5994c 100644 --- a/app/services/subscription_auto_purchase_service.py +++ b/app/services/subscription_auto_purchase_service.py @@ -347,6 +347,9 @@ async def _auto_extend_subscription( *, bot: Bot | None = None, ) -> bool: + # Lazy import to avoid circular dependency + from app.cabinet.routes.websocket import notify_user_subscription_renewed + try: prepared = await _prepare_auto_extend_context(db, user, cart_data) except Exception as error: # pragma: no cover - defensive logging @@ -559,6 +562,20 @@ async def _auto_extend_subscription( _format_user_id(user), ) + # Send WebSocket notification to cabinet frontend + try: + await notify_user_subscription_renewed( + user_id=user.id, + new_expires_at=new_end_date.isoformat() if new_end_date else '', + amount_kopeks=prepared.price_kopeks, + ) + except Exception as ws_error: + logger.warning( + '⚠️ Автопокупка: не удалось отправить WS уведомление о продлении для %s: %s', + _format_user_id(user), + ws_error, + ) + return True @@ -570,6 +587,11 @@ async def _auto_purchase_tariff( bot: Bot | None = None, ) -> bool: """Автоматическая покупка периодного тарифа из сохранённой корзины.""" + # Lazy imports to avoid circular dependency + from app.cabinet.routes.websocket import ( + notify_user_subscription_activated, + notify_user_subscription_renewed, + ) from app.database.crud.server_squad import get_all_server_squads from app.database.crud.subscription import ( create_paid_subscription, @@ -814,6 +836,29 @@ async def _auto_purchase_tariff( _format_user_id(user), ) + # Send WebSocket notification to cabinet frontend + try: + if existing_subscription: + # Renewal of existing subscription + await notify_user_subscription_renewed( + user_id=user.id, + new_expires_at=subscription.end_date.isoformat() if subscription.end_date else '', + amount_kopeks=final_price, + ) + else: + # New subscription activation + await notify_user_subscription_activated( + user_id=user.id, + expires_at=subscription.end_date.isoformat() if subscription.end_date else '', + tariff_name=tariff.name, + ) + except Exception as ws_error: + logger.warning( + '⚠️ Автопокупка тарифа: не удалось отправить WS уведомление для %s: %s', + _format_user_id(user), + ws_error, + ) + return True @@ -827,6 +872,11 @@ async def _auto_purchase_daily_tariff( """Автоматическая покупка суточного тарифа из сохранённой корзины.""" from datetime import datetime, timedelta + # Lazy imports to avoid circular dependency + from app.cabinet.routes.websocket import ( + notify_user_subscription_activated, + notify_user_subscription_renewed, + ) from app.database.crud.server_squad import get_all_server_squads from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id from app.database.crud.tariff import get_tariff_by_id @@ -1051,6 +1101,29 @@ async def _auto_purchase_daily_tariff( _format_user_id(user), ) + # Send WebSocket notification to cabinet frontend + try: + if existing_subscription: + # Renewal/upgrade of existing subscription + await notify_user_subscription_renewed( + user_id=user.id, + new_expires_at=subscription.end_date.isoformat() if subscription.end_date else '', + amount_kopeks=daily_price, + ) + else: + # New subscription activation + await notify_user_subscription_activated( + user_id=user.id, + expires_at=subscription.end_date.isoformat() if subscription.end_date else '', + tariff_name=tariff.name, + ) + except Exception as ws_error: + logger.warning( + '⚠️ Автопокупка суточного тарифа: не удалось отправить WS уведомление для %s: %s', + _format_user_id(user), + ws_error, + ) + return True @@ -1061,6 +1134,11 @@ async def auto_purchase_saved_cart_after_topup( bot: Bot | None = None, ) -> bool: """Attempts to automatically purchase a subscription from a saved cart.""" + # Lazy imports to avoid circular dependency + from app.cabinet.routes.websocket import ( + notify_user_subscription_activated, + notify_user_subscription_renewed, + ) if not settings.is_auto_purchase_after_topup_enabled(): return False @@ -1243,6 +1321,29 @@ async def auto_purchase_saved_cart_after_topup( _format_user_id(user), ) + # Send WebSocket notification to cabinet frontend + try: + if was_trial_conversion: + # Trial conversion = activation + await notify_user_subscription_activated( + user_id=user.id, + expires_at=subscription.end_date.isoformat() if subscription and subscription.end_date else '', + tariff_name='', + ) + else: + # Regular purchase = renewal or new activation + await notify_user_subscription_renewed( + user_id=user.id, + new_expires_at=subscription.end_date.isoformat() if subscription and subscription.end_date else '', + amount_kopeks=pricing.final_total, + ) + except Exception as ws_error: + logger.warning( + '⚠️ Автопокупка: не удалось отправить WS уведомление для %s: %s', + _format_user_id(user), + ws_error, + ) + return True @@ -1273,6 +1374,11 @@ async def auto_activate_subscription_after_topup( """ from datetime import datetime + # Lazy imports to avoid circular dependency + from app.cabinet.routes.websocket import ( + notify_user_subscription_activated, + notify_user_subscription_renewed, + ) from app.database.crud.server_squad import get_available_server_squads, get_server_ids_by_uuids from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id from app.database.crud.transaction import create_transaction @@ -1397,6 +1503,20 @@ async def auto_activate_subscription_after_topup( best_price, ) + # Send WebSocket notification to cabinet frontend + try: + await notify_user_subscription_renewed( + user_id=user.id, + new_expires_at=result.subscription.end_date.isoformat() if result.subscription.end_date else '', + amount_kopeks=best_price, + ) + except Exception as ws_error: + logger.warning( + '⚠️ Автоактивация: не удалось отправить WS уведомление о продлении для %s: %s', + _format_user_id(user), + ws_error, + ) + # Уведомление пользователю (только для Telegram-пользователей) if bot and user.telegram_id: try: @@ -1475,6 +1595,20 @@ async def auto_activate_subscription_after_topup( best_price, ) + # Send WebSocket notification to cabinet frontend + try: + await notify_user_subscription_activated( + user_id=user.id, + expires_at=new_subscription.end_date.isoformat() if new_subscription.end_date else '', + tariff_name='', + ) + except Exception as ws_error: + logger.warning( + '⚠️ Автоактивация: не удалось отправить WS уведомление об активации для %s: %s', + _format_user_id(user), + ws_error, + ) + # Уведомление пользователю (только для Telegram-пользователей) if bot and user.telegram_id: try: