From b2cf4aaa91f3fb63dca7e70645cadb75aa158cfe Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 2 Mar 2026 20:53:59 +0300 Subject: [PATCH] fix: eliminate double panel API call on tariff change, harden cart notification Bug 1 improvement: Replaced double API call pattern (sync + update_remnawave_user) with single _sync_subscription_to_panel call that accepts reset_traffic parameter. This prevents TRIAL status being overwritten to EXPIRED by the second call's different status computation logic. Bug 2 improvement: Moved keyboard construction inside try block to prevent AttributeError crash if locale keys are missing. Switched button text from attribute access (texts.KEY) to defensive texts.get('KEY', fallback). Added empty template guard to prevent sending empty messages to Telegram API. --- app/cabinet/routes/admin_users.py | 35 +++++++++++++++++++---------- app/services/payment/common.py | 37 +++++++++++++++++++------------ 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/app/cabinet/routes/admin_users.py b/app/cabinet/routes/admin_users.py index 8addcfc9..c7adb187 100644 --- a/app/cabinet/routes/admin_users.py +++ b/app/cabinet/routes/admin_users.py @@ -206,10 +206,17 @@ async def _build_subscription_info_async(db: AsyncSession, subscription: Subscri return info -async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription: Subscription) -> dict: +async def _sync_subscription_to_panel( + db: AsyncSession, + user: User, + subscription: Subscription, + reset_traffic: bool = False, + reset_traffic_reason: str | None = None, +) -> dict: """ Sync user subscription to Remnawave panel. Creates user if not exists, updates if exists. + Optionally resets traffic after sync. Returns dict with changes/errors. """ try: @@ -335,6 +342,16 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription changes['panel_uuid'] = new_panel_user.uuid logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid) + # Reset traffic on panel if requested + if reset_traffic and user.remnawave_uuid: + try: + await api.reset_user_traffic(user.remnawave_uuid) + changes['traffic_reset'] = True + reason_text = f' ({reset_traffic_reason})' if reset_traffic_reason else '' + logger.info('Reset RemnaWave traffic for user', user_id=user.id, reason=reason_text) + except Exception as reset_exc: + logger.warning('Failed to reset RemnaWave traffic', user_id=user.id, error=reset_exc) + user.last_remnawave_sync = datetime.now(UTC) await db.commit() @@ -1075,17 +1092,11 @@ async def update_user_subscription( # Синхронизируем с RemnaWave (discovery/create + сброс трафика по админ-настройке) try: - result = await _sync_subscription_to_panel(db, user, subscription) - if settings.RESET_TRAFFIC_ON_TARIFF_SWITCH and result.get('action') in ('updated', 'created'): - from app.services.subscription_service import SubscriptionService - - subscription_service = SubscriptionService() - await subscription_service.update_remnawave_user( - db, - subscription, - reset_traffic=True, - reset_reason='смена тарифа (cabinet admin)', - ) + await _sync_subscription_to_panel( + db, user, subscription, + reset_traffic=settings.RESET_TRAFFIC_ON_TARIFF_SWITCH, + reset_traffic_reason='смена тарифа (cabinet admin)', + ) except Exception as e: logger.error('Failed to sync tariff switch with RemnaWave', error=e) diff --git a/app/services/payment/common.py b/app/services/payment/common.py index 334a68dc..e66c9155 100644 --- a/app/services/payment/common.py +++ b/app/services/payment/common.py @@ -352,21 +352,30 @@ async def send_cart_notification_after_topup( amount=fmt(amount_kopeks), balance=fmt(balance), cart_total=fmt(cart_total), missing=fmt(missing), ) - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT, - callback_data='return_to_saved_cart', - ) - ], - [types.InlineKeyboardButton(text=texts.MY_BALANCE_BUTTON, callback_data='menu_balance')], - [types.InlineKeyboardButton(text=texts.MAIN_MENU_BUTTON, callback_data='back_to_menu')], - ] - ) + if not message_text: + logger.warning('Missing cart notification template', language=getattr(user, 'language', 'ru')) + return False sent = False try: + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text=texts.get('RETURN_TO_SUBSCRIPTION_CHECKOUT', '⬅️ Checkout'), + callback_data='return_to_saved_cart', + ) + ], + [types.InlineKeyboardButton( + text=texts.get('MY_BALANCE_BUTTON', '💰 Balance'), + callback_data='menu_balance', + )], + [types.InlineKeyboardButton( + text=texts.get('MAIN_MENU_BUTTON', '🏠 Menu'), + callback_data='back_to_menu', + )], + ] + ) await bot.send_message( chat_id=user.telegram_id, text=message_text, @@ -374,10 +383,10 @@ async def send_cart_notification_after_topup( parse_mode='HTML', ) sent = True - logger.info('Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю', user_id=user.id) + logger.info('Sent cart notification to user', user_id=user.id) except Exception as send_error: logger.error( - 'Ошибка отправки уведомления о корзине пользователю', + 'Failed to send cart notification to user', user_id=user.id, error=send_error, )