Revert "Prevent duplicate edits when resuming saved subscription carts"

This commit is contained in:
Egor
2025-11-02 06:11:59 +03:00
committed by GitHub
parent aac5fabbca
commit 90a054e942
11 changed files with 19 additions and 144 deletions

View File

@@ -853,42 +853,14 @@ async def return_to_saved_cart(
summary_text = "\n".join(summary_lines)
# Устанавливаем данные в FSM для продолжения процесса
confirm_keyboard = get_subscription_confirm_keyboard_with_cart(db_user.language)
current_message_text = getattr(callback.message, "text", None)
if not current_message_text:
current_message_text = getattr(callback.message, "caption", "") or ""
try:
current_keyboard = callback.message.reply_markup
except AttributeError:
current_keyboard = None
markups_are_equal = False
if current_keyboard is None:
markups_are_equal = confirm_keyboard is None
else:
try:
markups_are_equal = current_keyboard == confirm_keyboard
except Exception: # pragma: no cover - защитная ветка на случай несовместимых объектов
try:
current_dump = current_keyboard.model_dump()
confirm_dump = confirm_keyboard.model_dump()
except Exception: # pragma: no cover - дополнительная защита от несовместимых объектов
markups_are_equal = False
else:
markups_are_equal = current_dump == confirm_dump
await state.set_data(prepared_cart_data)
await state.set_state(SubscriptionStates.confirming_purchase)
if summary_text != current_message_text or not markups_are_equal:
await callback.message.edit_text(
summary_text,
reply_markup=confirm_keyboard,
parse_mode="HTML"
)
await callback.message.edit_text(
summary_text,
reply_markup=get_subscription_confirm_keyboard_with_cart(db_user.language),
parse_mode="HTML"
)
await callback.answer("✅ Корзина восстановлена!")

View File

@@ -356,13 +356,10 @@ def get_main_menu_keyboard(
paired_buttons.append(simple_purchase_button)
if show_resume_checkout or has_saved_cart:
resume_callback = (
"return_to_saved_cart" if has_saved_cart else "subscription_resume_checkout"
)
paired_buttons.append(
InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data=resume_callback,
callback_data="subscription_resume_checkout",
)
)
@@ -674,7 +671,7 @@ def get_insufficient_balance_keyboard(
return_row = [
InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart",
callback_data="subscription_resume_checkout",
)
]
insert_index = back_row_index if back_row_index is not None else len(keyboard.inline_keyboard)
@@ -806,7 +803,7 @@ def get_payment_methods_keyboard_with_cart(
keyboard.inline_keyboard.insert(-1, [ # Вставляем перед кнопкой "назад"
InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart"
callback_data="subscription_resume_checkout"
)
])

View File

@@ -23,7 +23,6 @@ from app.services.subscription_checkout_service import (
has_subscription_checkout_draft,
should_offer_checkout_resume,
)
from app.services.user_cart_service import user_cart_service
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
logger = logging.getLogger(__name__)
@@ -57,32 +56,14 @@ class PaymentCommonMixin:
# Если для пользователя есть незавершённый checkout, предлагаем вернуться к нему.
if user:
try:
has_saved_cart = await user_cart_service.has_user_cart(user.id)
except Exception as cart_error:
logger.warning(
"Не удалось проверить наличие сохраненной корзины у пользователя %s: %s",
user.id,
cart_error,
)
has_saved_cart = False
if has_saved_cart:
draft_exists = await has_subscription_checkout_draft(user.id)
if should_offer_checkout_resume(user, draft_exists):
keyboard_rows.append([
build_miniapp_or_callback_button(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart",
callback_data="subscription_resume_checkout",
)
])
else:
draft_exists = await has_subscription_checkout_draft(user.id)
if should_offer_checkout_resume(user, draft_exists):
keyboard_rows.append([
build_miniapp_or_callback_button(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="subscription_resume_checkout",
)
])
# Стандартные кнопки быстрого доступа к балансу и главному меню.
keyboard_rows.append([

View File

@@ -332,7 +332,7 @@ class CryptoBotPaymentMixin:
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart"
callback_data="subscription_resume_checkout"
)],
[types.InlineKeyboardButton(
text="💰 Мой баланс",

View File

@@ -366,7 +366,7 @@ class MulenPayPaymentMixin:
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart"
callback_data="subscription_resume_checkout"
)],
[types.InlineKeyboardButton(
text="💰 Мой баланс",

View File

@@ -473,7 +473,7 @@ class Pal24PaymentMixin:
"BALANCE_TOPUP_CART_BUTTON",
"🛒 Продолжить оформление",
),
callback_data="return_to_saved_cart",
callback_data="subscription_resume_checkout",
)
],
[

View File

@@ -584,7 +584,7 @@ class TelegramStarsMixin:
[
types.InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart",
callback_data="subscription_resume_checkout",
)
],
[

View File

@@ -561,7 +561,7 @@ class WataPaymentMixin:
[
types.InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart",
callback_data="subscription_resume_checkout",
)
],
[

View File

@@ -534,7 +534,7 @@ class YooKassaPaymentMixin:
[
types.InlineKeyboardButton(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="return_to_saved_cart",
callback_data="subscription_resume_checkout",
)
],
[

View File

@@ -882,7 +882,7 @@ async def test_process_pal24_postback_success(monkeypatch: pytest.MonkeyPatch) -
saved_cart_message = bot.sent_messages[-1]
reply_markup = saved_cart_message["kwargs"].get("reply_markup")
assert reply_markup is not None
assert reply_markup.inline_keyboard[0][0].kwargs["callback_data"] == "return_to_saved_cart"
assert reply_markup.inline_keyboard[0][0].kwargs["callback_data"] == "subscription_resume_checkout"
assert admin_calls

View File

@@ -1,7 +1,7 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, User as TgUser, Message, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.types import CallbackQuery, User as TgUser, Message
from sqlalchemy.ext.asyncio import AsyncSession
from app.handlers.subscription.purchase import save_cart_and_redirect_to_topup, return_to_saved_cart, clear_saved_cart
from app.handlers.subscription.autopay import handle_subscription_cancel
@@ -12,9 +12,6 @@ def mock_callback_query():
callback = AsyncMock(spec=CallbackQuery)
callback.message = AsyncMock(spec=Message)
callback.message.edit_text = AsyncMock()
callback.message.text = ""
callback.message.caption = None
callback.message.reply_markup = None
callback.answer = AsyncMock()
callback.data = "subscription_confirm"
return callback
@@ -146,78 +143,6 @@ async def test_return_to_saved_cart_success(mock_callback_query, mock_state, moc
mock_callback_query.answer.assert_called_once()
@pytest.mark.asyncio
async def test_return_to_saved_cart_skips_redundant_message_update(
mock_callback_query,
mock_state,
mock_user,
mock_db,
):
cart_data = {
'period_days': 30,
'countries': ['ru'],
'devices': 3,
'traffic_gb': 0,
'total_price': 30000,
'saved_cart': True,
'user_id': mock_user.id,
}
confirm_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="ok", callback_data="subscription_confirm")],
[InlineKeyboardButton(text="clear", callback_data="clear_saved_cart")],
])
patch_user_cart = patch('app.handlers.subscription.purchase.user_cart_service')
patch_countries = patch('app.handlers.subscription.purchase._get_available_countries')
patch_period = patch('app.handlers.subscription.purchase.format_period_description')
patch_texts = patch('app.localization.texts.get_texts')
patch_keyboard = patch(
'app.handlers.subscription.purchase.get_subscription_confirm_keyboard_with_cart',
return_value=confirm_keyboard,
)
patch_settings = patch('app.handlers.subscription.purchase.settings')
with patch_user_cart as mock_cart_service, patch_countries as mock_get_countries, \
patch_period as mock_format_period, patch_texts as mock_get_texts, \
patch_keyboard as _, patch_settings as mock_settings:
mock_cart_service.get_user_cart = AsyncMock(return_value=cart_data)
mock_get_countries.return_value = [{'uuid': 'ru', 'name': 'Russia'}]
mock_format_period.return_value = "30 дней"
mock_texts = AsyncMock()
mock_texts.format_price = lambda x: f"{x // 100}"
mock_get_texts.return_value = mock_texts
mock_settings.is_devices_selection_enabled.return_value = True
mock_settings.is_traffic_fixed.return_value = False
mock_user.balance_kopeks = 50000
expected_summary = "\n".join([
"🛒 Восстановленная корзина",
"",
"📅 Период: 30 дней",
"📊 Трафик: Безлимитный",
"🌍 Страны: Russia",
"📱 Устройства: 3",
"",
"💎 Общая стоимость: 300 ₽",
"",
"Подтверждаете покупку?",
])
mock_callback_query.message.text = expected_summary
mock_callback_query.message.reply_markup = confirm_keyboard
await return_to_saved_cart(mock_callback_query, mock_state, mock_user, mock_db)
mock_state.set_data.assert_called_once_with(cart_data)
mock_callback_query.message.edit_text.assert_not_called()
mock_callback_query.answer.assert_called_once_with("✅ Корзина восстановлена!")
@pytest.mark.asyncio
async def test_return_to_saved_cart_normalizes_devices_when_disabled(
mock_callback_query,