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