diff --git a/app/handlers/admin/rules.py b/app/handlers/admin/rules.py
index 5c068535..36aef825 100644
--- a/app/handlers/admin/rules.py
+++ b/app/handlers/admin/rules.py
@@ -1,4 +1,5 @@
import logging
+import re
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
@@ -11,6 +12,14 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.validators import get_html_help_text, validate_html_tags
+def _safe_preview(html_text: str, limit: int = 500) -> 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..e9969c13 100644
--- a/app/services/payment/common.py
+++ b/app/services/payment/common.py
@@ -7,16 +7,19 @@
from __future__ import annotations
+from datetime import datetime
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,
@@ -46,12 +49,26 @@ 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.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:
+ 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',
+ getattr(user, 'id', None),
+ db_error,
+ )
except Exception as error: # pragma: no cover - защитный код
logger.error(
'Ошибка загрузки подписки пользователя %s при построении клавиатуры после пополнения: %s',
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