From 40d8a6dc8baf3f0f7c30b0883898b4655a907eb5 Mon Sep 17 00:00:00 2001 From: Fringg Date: Tue, 10 Feb 2026 03:32:20 +0300 Subject: [PATCH 1/3] fix: safe HTML preview truncation and lazy-load subscription fallback Rules editor crashed when preview truncated mid-HTML tag (e.g.
cut to str: + """Создаёт превью текста, безопасно обрезая HTML-теги.""" + plain = re.sub(r'<[^>]+>', '', html_text) + if len(plain) <= limit: + return plain + return plain[:limit] + '...' + + logger = logging.getLogger(__name__) @@ -79,7 +88,7 @@ async def start_edit_rules(callback: types.CallbackQuery, db_user: User, state: try: current_rules = await get_current_rules_content(db, db_user.language) - preview = current_rules[:500] + ('...' if len(current_rules) > 500 else '') + preview = _safe_preview(current_rules, 500) text = ( '✏️ Редактирование правил\n\n' @@ -139,7 +148,7 @@ async def process_rules_edit(message: types.Message, db_user: User, state: FSMCo if len(preview_text) > 4000: preview_text = ( '📋 Предварительный просмотр новых правил:\n\n' - f'{new_rules[:500]}...\n\n' + f'{_safe_preview(new_rules, 500)}\n\n' f'⚠️ Внимание! Новые правила будут показываться всем пользователям.\n\n' f'Текст правил: {len(new_rules)} символов\n' f'Сохранить изменения?' diff --git a/app/services/payment/common.py b/app/services/payment/common.py index 3e97a631..68962844 100644 --- a/app/services/payment/common.py +++ b/app/services/payment/common.py @@ -11,12 +11,14 @@ from types import SimpleNamespace from typing import Any from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from sqlalchemy import select from sqlalchemy.exc import MissingGreenlet from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.crud.user import get_user_by_telegram_id -from app.database.database import get_db +from app.database.database import AsyncSessionLocal, get_db +from app.database.models import Subscription from app.localization.texts import get_texts from app.services.subscription_checkout_service import ( has_subscription_checkout_draft, @@ -37,7 +39,6 @@ class PaymentCommonMixin: # Определяем статус подписки, чтобы показать подходящую кнопку. has_active_subscription = False - subscription = None if user: try: subscription = user.subscription @@ -46,12 +47,24 @@ class PaymentCommonMixin: and not getattr(subscription, 'is_trial', False) and getattr(subscription, 'is_active', False) ) - except MissingGreenlet as error: - logger.warning( - 'Не удалось лениво загрузить подписку пользователя %s при построении клавиатуры после пополнения: %s', - getattr(user, 'id', None), - error, - ) + except MissingGreenlet: + # user вне сессии — загружаем подписку отдельным запросом + try: + async with AsyncSessionLocal() as session: + result = await session.execute( + select(Subscription.is_active, Subscription.is_trial) + .where(Subscription.user_id == user.id) + .limit(1) + ) + row = result.one_or_none() + if row: + has_active_subscription = bool(row.is_active and not row.is_trial) + except Exception as db_error: + logger.warning( + 'Не удалось загрузить подписку пользователя %s из БД: %s', + getattr(user, 'id', None), + db_error, + ) except Exception as error: # pragma: no cover - защитный код logger.error( 'Ошибка загрузки подписки пользователя %s при построении клавиатуры после пополнения: %s', From f0e7f8e3bec27d97a3f22445948b8dde37a92438 Mon Sep 17 00:00:00 2001 From: Fringg Date: Tue, 10 Feb 2026 03:44:34 +0300 Subject: [PATCH 2/3] fix: use actual DB columns for subscription fallback query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subscription.is_active is a Python property, not a column — query status/end_date/is_trial columns instead. Also restore subscription=None initialization to avoid UnboundLocalError on line 112. --- app/services/payment/common.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/services/payment/common.py b/app/services/payment/common.py index 68962844..e9969c13 100644 --- a/app/services/payment/common.py +++ b/app/services/payment/common.py @@ -7,6 +7,7 @@ from __future__ import annotations +from datetime import datetime from types import SimpleNamespace from typing import Any @@ -39,6 +40,7 @@ class PaymentCommonMixin: # Определяем статус подписки, чтобы показать подходящую кнопку. has_active_subscription = False + subscription = None if user: try: subscription = user.subscription @@ -52,13 +54,15 @@ class PaymentCommonMixin: try: async with AsyncSessionLocal() as session: result = await session.execute( - select(Subscription.is_active, Subscription.is_trial) + select(Subscription.status, Subscription.is_trial, Subscription.end_date) .where(Subscription.user_id == user.id) + .order_by(Subscription.created_at.desc()) .limit(1) ) row = result.one_or_none() if row: - has_active_subscription = bool(row.is_active and not row.is_trial) + is_active = row.status == 'active' and row.end_date > datetime.utcnow() + has_active_subscription = bool(is_active and not row.is_trial) except Exception as db_error: logger.warning( 'Не удалось загрузить подписку пользователя %s из БД: %s', From 994325360ca7665800177bfad8f831154f4d733f Mon Sep 17 00:00:00 2001 From: Fringg Date: Tue, 10 Feb 2026 03:50:21 +0300 Subject: [PATCH 3/3] fix: don't delete Heleket invoice message on status check _process_heleket_payload deleted the invoice message on every call, including manual "check status" presses. Now only deletes on final statuses (paid, cancel, fail, etc.) so the payment UI stays visible while the user is still waiting. Also includes subscription fallback query fix (actual DB columns). --- app/services/payment/heleket.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py index 6b7f39c3..27172c9e 100644 --- a/app/services/payment/heleket.py +++ b/app/services/payment/heleket.py @@ -260,7 +260,10 @@ class HeleketPaymentMixin: invoice_message = metadata.get('invoice_message') or {} invoice_message_removed = False - if getattr(self, 'bot', None) and invoice_message: + status_normalized = (status or '').lower() + is_final = status_normalized in {'paid', 'paid_over', 'cancel', 'fail', 'system_fail', 'refund_paid'} + + if getattr(self, 'bot', None) and invoice_message and is_final: chat_id = invoice_message.get('chat_id') message_id = invoice_message.get('message_id') if chat_id and message_id: @@ -300,7 +303,6 @@ class HeleketPaymentMixin: ) return updated_payment - status_normalized = (status or '').lower() if status_normalized not in {'paid', 'paid_over'}: logger.info('Heleket платеж %s в статусе %s, зачисление не требуется', updated_payment.uuid, status) return updated_payment