diff --git a/app/database/models.py b/app/database/models.py index 3fb28fb5..b4e9bc80 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -529,7 +529,6 @@ class User(Base): vless_uuid = Column(String(255), nullable=True) ss_password = Column(String(255), nullable=True) has_made_first_topup: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) - auto_purchase_after_topup_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) promo_group_id = Column(Integer, ForeignKey("promo_groups.id", ondelete="RESTRICT"), nullable=False, index=True) promo_group = relationship("PromoGroup", back_populates="users") poll_responses = relationship("PollResponse", back_populates="user") diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 4de323ca..7b51db85 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -21,7 +21,7 @@ from app.database.crud.subscription import ( update_subscription_autopay ) from app.database.crud.transaction import create_transaction -from app.database.crud.user import subtract_user_balance, update_user +from app.database.crud.user import subtract_user_balance from app.database.models import ( User, TransactionType, SubscriptionStatus, Subscription @@ -650,22 +650,6 @@ async def _edit_message_text_or_caption( raise - -def _get_auto_purchase_status_lines(texts, enabled: bool) -> tuple[str, str]: - status_text = texts.AUTO_PURCHASE_AFTER_TOPUP_STATUS.format( - status=( - texts.AUTO_PURCHASE_AFTER_TOPUP_STATUS_ENABLED - if enabled - else texts.AUTO_PURCHASE_AFTER_TOPUP_STATUS_DISABLED - ) - ) - status_hint = ( - texts.AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_ON - if enabled - else texts.AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_OFF - ) - return status_text, status_hint - async def save_cart_and_redirect_to_topup( callback: types.CallbackQuery, state: FSMContext, @@ -686,157 +670,20 @@ async def save_cart_and_redirect_to_topup( await user_cart_service.save_user_cart(db_user.id, cart_data) - auto_purchase_enabled = getattr(db_user, "auto_purchase_after_topup_enabled", False) - status_text, status_hint = _get_auto_purchase_status_lines(texts, auto_purchase_enabled) - await callback.message.edit_text( f"💰 Недостаточно средств для оформления подписки\n\n" f"Требуется: {texts.format_price(missing_amount)}\n" f"У вас: {texts.format_price(db_user.balance_kopeks)}\n\n" f"🛒 Ваша корзина сохранена!\n" - f"{status_text}\n" - f"{status_hint}\n\n" f"После пополнения баланса вы сможете вернуться к оформлению подписки.\n\n" f"Выберите способ пополнения:", reply_markup=get_payment_methods_keyboard_with_cart( db_user.language, missing_amount, - auto_purchase_enabled=auto_purchase_enabled, ), parse_mode="HTML" ) - -def _rebuild_topup_prompt_text( - texts, - missing_amount: int, - balance: int, - *, - status_text: str, - status_hint: str, -) -> str: - return ( - f"💰 Недостаточно средств для оформления подписки\n\n" - f"Требуется: {texts.format_price(missing_amount)}\n" - f"У вас: {texts.format_price(balance)}\n\n" - f"🛒 Ваша корзина сохранена!\n" - f"{status_text}\n" - f"{status_hint}\n\n" - f"После пополнения баланса вы сможете вернуться к оформлению подписки.\n\n" - f"Выберите способ пополнения:" - ) - - -def _rebuild_insufficient_text( - texts, - total_price: int, - balance: int, - missing_amount: int, - *, - status_text: str, - status_hint: str, -) -> str: - return ( - f"❌ Все еще недостаточно средств\n\n" - f"Требуется: {texts.format_price(total_price)}\n" - f"У вас: {texts.format_price(balance)}\n" - f"Не хватает: {texts.format_price(missing_amount)}\n\n" - f"{status_text}\n{status_hint}" - ) - - -async def toggle_auto_purchase_after_topup( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, -): - enable = callback.data == "auto_purchase_topup_toggle_on" - - try: - if enable != getattr(db_user, "auto_purchase_after_topup_enabled", False): - db_user = await update_user( - db, - db_user, - auto_purchase_after_topup_enabled=enable, - ) - except Exception as error: - logger.error("Не удалось обновить настройку автопокупки: %s", error) - await callback.answer("⚠️ Не удалось обновить настройку", show_alert=True) - return - - texts = get_texts(db_user.language) - notice = ( - texts.AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_ON - if enable - else texts.AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_OFF - ) - await callback.answer(notice, show_alert=False) - - cart_data = await user_cart_service.get_user_cart(db_user.id) - if not cart_data: - # Обновляем только клавиатуру, если корзина не найдена - try: - await callback.message.edit_reply_markup( - reply_markup=get_payment_methods_keyboard_with_cart( - db_user.language, - 0, - auto_purchase_enabled=enable, - ) - ) - except Exception: - pass - return - - total_price = cart_data.get('total_price', 0) - missing_amount = cart_data.get('missing_amount', 0) - - if total_price: - recalculated_missing = max(0, total_price - db_user.balance_kopeks) - missing_amount = recalculated_missing - cart_data['missing_amount'] = missing_amount - await user_cart_service.save_user_cart(db_user.id, cart_data) - - status_text, status_hint = _get_auto_purchase_status_lines(texts, enable) - - message_text = callback.message.text or callback.message.caption or "" - reply_markup: InlineKeyboardMarkup - - if "Ваша корзина сохранена" in message_text: - new_text = _rebuild_topup_prompt_text( - texts, - missing_amount, - db_user.balance_kopeks, - status_text=status_text, - status_hint=status_hint, - ) - reply_markup = get_payment_methods_keyboard_with_cart( - db_user.language, - missing_amount, - auto_purchase_enabled=enable, - ) - else: - total_value = total_price or (db_user.balance_kopeks + missing_amount) - new_text = _rebuild_insufficient_text( - texts, - total_value, - db_user.balance_kopeks, - missing_amount, - status_text=status_text, - status_hint=status_hint, - ) - reply_markup = get_insufficient_balance_keyboard_with_cart( - db_user.language, - missing_amount, - auto_purchase_enabled=enable, - ) - - await _edit_message_text_or_caption( - callback.message, - new_text, - reply_markup=reply_markup, - parse_mode="HTML", - ) - async def return_to_saved_cart( callback: types.CallbackQuery, state: FSMContext, @@ -855,18 +702,14 @@ async def return_to_saved_cart( if db_user.balance_kopeks < total_price: missing_amount = total_price - db_user.balance_kopeks - auto_purchase_enabled = getattr(db_user, "auto_purchase_after_topup_enabled", False) - status_text, status_hint = _get_auto_purchase_status_lines(texts, auto_purchase_enabled) await callback.message.edit_text( f"❌ Все еще недостаточно средств\n\n" f"Требуется: {texts.format_price(total_price)}\n" f"У вас: {texts.format_price(db_user.balance_kopeks)}\n" - f"Не хватает: {texts.format_price(missing_amount)}\n\n" - f"{status_text}\n{status_hint}", + f"Не хватает: {texts.format_price(missing_amount)}", reply_markup=get_insufficient_balance_keyboard_with_cart( db_user.language, missing_amount, - auto_purchase_enabled=auto_purchase_enabled, ) ) return @@ -1754,11 +1597,6 @@ async def confirm_purchase( missing=texts.format_price(missing_kopeks), ) - auto_purchase_enabled = getattr(db_user, "auto_purchase_after_topup_enabled", False) - status_text, status_hint = _get_auto_purchase_status_lines(texts, auto_purchase_enabled) - - message_text = f"{message_text}\n\n{status_text}\n{status_hint}" - # Сохраняем данные корзины в Redis перед переходом к пополнению cart_data = { **data, @@ -1776,8 +1614,7 @@ async def confirm_purchase( db_user.language, resume_callback=resume_callback, amount_kopeks=missing_kopeks, - has_saved_cart=True, # Указываем, что есть сохраненная корзина - auto_purchase_enabled=auto_purchase_enabled, + has_saved_cart=True # Указываем, что есть сохраненная корзина ), parse_mode="HTML", ) @@ -1812,18 +1649,12 @@ async def confirm_purchase( missing=texts.format_price(missing_kopeks), ) - auto_purchase_enabled = getattr(db_user, "auto_purchase_after_topup_enabled", False) - status_text, status_hint = _get_auto_purchase_status_lines(texts, auto_purchase_enabled) - - message_text = f"{message_text}\n\n{status_text}\n{status_hint}" - await callback.message.edit_text( message_text, reply_markup=get_insufficient_balance_keyboard( db_user.language, resume_callback=resume_callback, amount_kopeks=missing_kopeks, - auto_purchase_enabled=auto_purchase_enabled, ), parse_mode="HTML", ) @@ -2381,11 +2212,6 @@ def register_handlers(dp: Dispatcher): F.data == "clear_saved_cart", ) - dp.callback_query.register( - toggle_auto_purchase_after_topup, - F.data.in_(["auto_purchase_topup_toggle_on", "auto_purchase_topup_toggle_off"]), - ) - dp.callback_query.register( handle_autopay_menu, F.data == "subscription_autopay" diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 94731644..46a64f05 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -643,8 +643,6 @@ def get_insufficient_balance_keyboard( resume_callback: str | None = None, amount_kopeks: int | None = None, has_saved_cart: bool = False, # Новый параметр для указания наличия сохраненной корзины - *, - auto_purchase_enabled: bool | None = None, ) -> InlineKeyboardMarkup: texts = get_texts(language) @@ -665,44 +663,25 @@ def get_insufficient_balance_keyboard( ) back_row_index = len(keyboard.inline_keyboard) - 1 - insert_index = back_row_index if back_row_index is not None else len(keyboard.inline_keyboard) - rows_to_insert: list[list[InlineKeyboardButton]] = [] - - if auto_purchase_enabled is not None: - toggle_row = [ - InlineKeyboardButton( - text=( - texts.AUTO_PURCHASE_AFTER_TOPUP_DISABLE_BUTTON - if auto_purchase_enabled - else texts.AUTO_PURCHASE_AFTER_TOPUP_ENABLE_BUTTON - ), - callback_data=( - "auto_purchase_topup_toggle_off" - if auto_purchase_enabled - else "auto_purchase_topup_toggle_on" - ), - ) - ] - rows_to_insert.append(toggle_row) - # Если есть сохраненная корзина, добавляем кнопку возврата к оформлению if has_saved_cart: - rows_to_insert.append([ + return_row = [ InlineKeyboardButton( text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT, callback_data="subscription_resume_checkout", ) - ]) + ] + insert_index = back_row_index if back_row_index is not None else len(keyboard.inline_keyboard) + keyboard.inline_keyboard.insert(insert_index, return_row) elif resume_callback: - rows_to_insert.append([ + return_row = [ InlineKeyboardButton( text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT, callback_data=resume_callback, ) - ]) - - for offset, row in enumerate(rows_to_insert): - keyboard.inline_keyboard.insert(insert_index + offset, row) + ] + insert_index = back_row_index if back_row_index is not None else len(keyboard.inline_keyboard) + keyboard.inline_keyboard.insert(insert_index, return_row) return keyboard @@ -805,34 +784,10 @@ def get_subscription_keyboard( def get_payment_methods_keyboard_with_cart( language: str = "ru", amount_kopeks: int = 0, - *, - auto_purchase_enabled: bool | None = None, ) -> InlineKeyboardMarkup: texts = get_texts(language) keyboard = get_payment_methods_keyboard(amount_kopeks, language) - toggle_row_insert_index = 0 - - if auto_purchase_enabled is not None: - keyboard.inline_keyboard.insert( - toggle_row_insert_index, - [ - InlineKeyboardButton( - text=( - texts.AUTO_PURCHASE_AFTER_TOPUP_DISABLE_BUTTON - if auto_purchase_enabled - else texts.AUTO_PURCHASE_AFTER_TOPUP_ENABLE_BUTTON - ), - callback_data=( - "auto_purchase_topup_toggle_off" - if auto_purchase_enabled - else "auto_purchase_topup_toggle_on" - ), - ) - ], - ) - toggle_row_insert_index += 1 - # Добавляем кнопку "Очистить корзину" keyboard.inline_keyboard.append([ InlineKeyboardButton( diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 29e30c58..4e9083e4 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -780,16 +780,6 @@ "BALANCE_SUPPORT_REQUEST": "🛠️ Request via support", "BALANCE_TOPUP": "💳 Top up balance", "BALANCE_TOPUP_CART_REMINDER_DETAILED": "\\n💡 Balance top-up required\\n\\nYour cart contains items totaling {total_amount}, but your current balance is insufficient.\\n\\n💳 Top up your balance to complete the purchase.\\n\\nChoose a top-up method:", - "AUTO_PURCHASE_AFTER_TOPUP_STATUS": "🤖 Auto purchase after top-up: {status}", - "AUTO_PURCHASE_AFTER_TOPUP_STATUS_ENABLED": "enabled", - "AUTO_PURCHASE_AFTER_TOPUP_STATUS_DISABLED": "disabled", - "AUTO_PURCHASE_AFTER_TOPUP_ENABLE_BUTTON": "🤖 Enable auto purchase", - "AUTO_PURCHASE_AFTER_TOPUP_DISABLE_BUTTON": "🚫 Disable auto purchase", - "AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_ON": "🤖 Auto purchase enabled. Your subscription will be paid automatically after topping up.", - "AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_OFF": "🤖 Auto purchase disabled.", - "AUTO_PURCHASE_AFTER_TOPUP_SUCCESS": "🤖 Auto purchase completed\\n\\nYour subscription for {period} was paid automatically. Charged: {amount}.", - "AUTO_PURCHASE_AFTER_TOPUP_SUCCESS_WITH_DISCOUNT": "🤖 Auto purchase completed\\n\\nYour subscription for {period} was paid automatically. Charged: {amount}. Discount: {discount}.", - "AUTO_PURCHASE_AFTER_TOPUP_INSUFFICIENT": "🤖 Auto purchase failed — balance is still insufficient.", "BALANCE_TOP_UP": "💳 Top up", "BLOCK_BY_TIME": "⏳ Temporary block", "BLOCK_FOREVER": "🚫 Block permanently", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 7e08eaf8..81d732a4 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -780,16 +780,6 @@ "BALANCE_SUPPORT_REQUEST": "🛠️ Запрос через поддержку", "BALANCE_TOPUP": "💳 Пополнить баланс", "BALANCE_TOPUP_CART_REMINDER_DETAILED": "\n💡 Требуется пополнение баланса\n\nВ вашей корзине находятся товары на общую сумму {total_amount}, но на балансе недостаточно средств.\n\n💳 Пополните баланс, чтобы завершить покупку.\n\nВыберите способ пополнения:", - "AUTO_PURCHASE_AFTER_TOPUP_STATUS": "🤖 Автопокупка после пополнения: {status}", - "AUTO_PURCHASE_AFTER_TOPUP_STATUS_ENABLED": "включена", - "AUTO_PURCHASE_AFTER_TOPUP_STATUS_DISABLED": "выключена", - "AUTO_PURCHASE_AFTER_TOPUP_ENABLE_BUTTON": "🤖 Включить автопокупку", - "AUTO_PURCHASE_AFTER_TOPUP_DISABLE_BUTTON": "🚫 Отключить автопокупку", - "AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_ON": "🤖 Автопокупка включена. После пополнения баланс будет списан автоматически.", - "AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_OFF": "🤖 Автопокупка отключена.", - "AUTO_PURCHASE_AFTER_TOPUP_SUCCESS": "🤖 Автопокупка выполнена\n\nПодписка на {period} оплачена автоматически. Списано: {amount}.", - "AUTO_PURCHASE_AFTER_TOPUP_SUCCESS_WITH_DISCOUNT": "🤖 Автопокупка выполнена\n\nПодписка на {period} оплачена автоматически. Списано: {amount}. Скидка: {discount}.", - "AUTO_PURCHASE_AFTER_TOPUP_INSUFFICIENT": "🤖 Автопокупка не выполнена — на балансе все еще недостаточно средств.", "BALANCE_TOP_UP": "💳 Пополнить", "BLOCK_BY_TIME": "⏳ Блокировка по времени", "BLOCK_FOREVER": "🚫 Заблокировать", diff --git a/app/services/auto_purchase_service.py b/app/services/auto_purchase_service.py deleted file mode 100644 index b8bce2f0..00000000 --- a/app/services/auto_purchase_service.py +++ /dev/null @@ -1,307 +0,0 @@ -import logging -from dataclasses import dataclass -from datetime import datetime, timedelta -from typing import List, Optional - -from aiogram import Bot -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from sqlalchemy.ext.asyncio import AsyncSession - -from app.config import settings -from app.database.crud.server_squad import add_user_to_servers, get_server_ids_by_uuids -from app.database.crud.subscription import add_subscription_servers, create_paid_subscription -from app.database.crud.transaction import create_transaction -from app.database.crud.user import subtract_user_balance -from app.database.models import SubscriptionStatus, TransactionType, User -from app.keyboards.inline import get_insufficient_balance_keyboard_with_cart -from app.localization.texts import get_texts -from app.services.admin_notification_service import AdminNotificationService -from app.services.subscription_checkout_service import clear_subscription_checkout_draft -from app.services.subscription_service import SubscriptionService -from app.services.user_cart_service import user_cart_service -from app.utils.pricing_utils import format_period_description -from app.utils.subscription_utils import get_display_subscription_link -from app.utils.user_utils import mark_user_as_had_paid_subscription - -logger = logging.getLogger(__name__) - - -@dataclass -class AutoPurchaseResult: - triggered: bool - success: bool - - -def _get_auto_purchase_status_lines(texts, enabled: bool) -> tuple[str, str]: - status_text = texts.AUTO_PURCHASE_AFTER_TOPUP_STATUS.format( - status=( - texts.AUTO_PURCHASE_AFTER_TOPUP_STATUS_ENABLED - if enabled - else texts.AUTO_PURCHASE_AFTER_TOPUP_STATUS_DISABLED - ) - ) - status_hint = ( - texts.AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_ON - if enabled - else texts.AUTO_PURCHASE_AFTER_TOPUP_TOGGLED_OFF - ) - return status_text, status_hint - - -def _build_autopurchase_failure_text(texts, total_price: int, balance: int, missing: int, enabled: bool) -> str: - status_text, status_hint = _get_auto_purchase_status_lines(texts, enabled) - return ( - f"{texts.AUTO_PURCHASE_AFTER_TOPUP_INSUFFICIENT}\n\n" - f"Стоимость: {texts.format_price(total_price)}\n" - f"На балансе: {texts.format_price(balance)}\n" - f"Не хватает: {texts.format_price(missing)}\n\n" - f"{status_text}\n{status_hint}" - ) - - -def _build_autopurchase_success_prefix(texts, language: str, period_days: int, final_price: int, discount_value: int) -> str: - period_display = format_period_description(period_days, language) - if discount_value > 0: - return texts.AUTO_PURCHASE_AFTER_TOPUP_SUCCESS_WITH_DISCOUNT.format( - period=period_display, - amount=texts.format_price(final_price), - discount=texts.format_price(discount_value), - ) - return texts.AUTO_PURCHASE_AFTER_TOPUP_SUCCESS.format( - period=period_display, - amount=texts.format_price(final_price), - ) - - -async def try_auto_purchase_after_topup( - db: AsyncSession, - user: User, - bot: Optional[Bot], -) -> AutoPurchaseResult: - if not getattr(user, "auto_purchase_after_topup_enabled", False): - return AutoPurchaseResult(triggered=False, success=False) - - cart_data = await user_cart_service.get_user_cart(user.id) - if not cart_data: - return AutoPurchaseResult(triggered=False, success=False) - - texts = get_texts(user.language) - - from app.handlers.subscription.pricing import _prepare_subscription_summary - - try: - _, prepared_data = await _prepare_subscription_summary(user, cart_data, texts) - except ValueError as error: - logger.error("Не удалось восстановить корзину для автопокупки: %s", error) - await user_cart_service.delete_user_cart(user.id) - return AutoPurchaseResult(triggered=True, success=False) - - final_price = prepared_data.get('total_price', 0) - promo_offer_discount_value = prepared_data.get('promo_offer_discount_value', 0) - promo_offer_discount_percent = prepared_data.get('promo_offer_discount_percent', 0) - - await db.refresh(user) - - if user.balance_kopeks < final_price: - missing = final_price - user.balance_kopeks - failure_text = _build_autopurchase_failure_text( - texts, - final_price, - user.balance_kopeks, - missing, - True, - ) - if bot: - try: - await bot.send_message( - chat_id=user.telegram_id, - text=failure_text, - parse_mode="HTML", - reply_markup=get_insufficient_balance_keyboard_with_cart( - user.language, - missing, - auto_purchase_enabled=True, - ), - ) - except Exception as send_error: - logger.error("Не удалось отправить уведомление об автопокупке: %s", send_error) - return AutoPurchaseResult(triggered=True, success=False) - - success = await subtract_user_balance( - db, - user, - final_price, - f"Покупка подписки на {prepared_data['period_days']} дней (авто)", - consume_promo_offer=promo_offer_discount_value > 0, - ) - - if not success: - await db.refresh(user) - missing = max(0, final_price - user.balance_kopeks) - failure_text = _build_autopurchase_failure_text( - texts, - final_price, - user.balance_kopeks, - missing, - True, - ) - if bot: - try: - await bot.send_message( - chat_id=user.telegram_id, - text=failure_text, - parse_mode="HTML", - reply_markup=get_insufficient_balance_keyboard_with_cart( - user.language, - missing, - auto_purchase_enabled=True, - ), - ) - except Exception as send_error: - logger.error("Не удалось отправить уведомление об автопокупке: %s", send_error) - return AutoPurchaseResult(triggered=True, success=False) - - final_traffic_gb = prepared_data.get('final_traffic_gb', prepared_data.get('traffic_gb', 0)) - server_prices = prepared_data.get('server_prices_for_period', []) - - existing_subscription = user.subscription - was_trial_conversion = False - current_time = datetime.utcnow() - - if existing_subscription: - bonus_period = timedelta() - if existing_subscription.is_trial: - was_trial_conversion = True - trial_duration = (current_time - existing_subscription.start_date).days - if settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID and existing_subscription.end_date: - remaining_trial_delta = existing_subscription.end_date - current_time - if remaining_trial_delta.total_seconds() > 0: - bonus_period = remaining_trial_delta - - existing_subscription.is_trial = False - existing_subscription.status = SubscriptionStatus.ACTIVE.value - existing_subscription.traffic_limit_gb = final_traffic_gb - existing_subscription.device_limit = prepared_data['devices'] - existing_subscription.connected_squads = prepared_data['countries'] - existing_subscription.start_date = current_time - existing_subscription.end_date = current_time + timedelta(days=prepared_data['period_days']) + bonus_period - existing_subscription.updated_at = current_time - existing_subscription.traffic_used_gb = 0.0 - - await db.commit() - await db.refresh(existing_subscription) - subscription = existing_subscription - else: - subscription = await create_paid_subscription( - db=db, - user_id=user.id, - duration_days=prepared_data['period_days'], - traffic_limit_gb=final_traffic_gb, - device_limit=prepared_data['devices'], - connected_squads=prepared_data['countries'], - ) - - await mark_user_as_had_paid_subscription(db, user) - - server_ids = await get_server_ids_by_uuids(db, prepared_data['countries']) - if server_ids: - await add_subscription_servers(db, subscription, server_ids, server_prices) - await add_user_to_servers(db, server_ids) - - await db.refresh(user) - - subscription_service = SubscriptionService() - if user.remnawave_uuid: - remnawave_user = await subscription_service.update_remnawave_user( - db, - subscription, - reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT, - reset_reason="автопокупка подписки", - ) - else: - remnawave_user = await subscription_service.create_remnawave_user( - db, - subscription, - reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT, - reset_reason="автопокупка подписки", - ) - if not remnawave_user: - remnawave_user = await subscription_service.create_remnawave_user( - db, - subscription, - reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT, - reset_reason="автопокупка подписки (повтор)", - ) - - transaction = await create_transaction( - db=db, - user_id=user.id, - type=TransactionType.SUBSCRIPTION_PAYMENT, - amount_kopeks=final_price, - description=f"Подписка на {prepared_data['period_days']} дней (авто)", - ) - - if bot: - try: - notification_service = AdminNotificationService(bot) - await notification_service.send_subscription_purchase_notification( - db, - user, - subscription, - transaction, - prepared_data['period_days'], - was_trial_conversion, - ) - except Exception as notify_error: - logger.error("Ошибка отправки уведомления админам об автопокупке: %s", notify_error) - - await db.refresh(user) - await db.refresh(subscription) - - subscription_link = get_display_subscription_link(subscription) - hide_subscription_link = settings.should_hide_subscription_link() - - auto_prefix = _build_autopurchase_success_prefix( - texts, - user.language, - prepared_data['period_days'], - final_price, - promo_offer_discount_value, - ) - - instruction_text = texts.t( - "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT", - "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве", - ) - success_text = f"{texts.SUBSCRIPTION_PURCHASED}\n\n{auto_prefix}\n\n{instruction_text}" - - if bot: - rows: List[List[InlineKeyboardButton]] = [] - if subscription_link and not hide_subscription_link: - rows.append([ - InlineKeyboardButton( - text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), - url=subscription_link, - ) - ]) - rows.append([ - InlineKeyboardButton(text=texts.MENU_SUBSCRIPTION, callback_data="menu_subscription") - ]) - rows.append([ - InlineKeyboardButton(text=texts.BACK_TO_MAIN_MENU_BUTTON, callback_data="back_to_menu") - ]) - - try: - await bot.send_message( - chat_id=user.telegram_id, - text=success_text, - parse_mode="HTML", - reply_markup=InlineKeyboardMarkup(inline_keyboard=rows), - ) - except Exception as send_error: - logger.error("Не удалось отправить сообщение об автопокупке: %s", send_error) - - await clear_subscription_checkout_draft(user.id) - await user_cart_service.delete_user_cart(user.id) - - return AutoPurchaseResult(triggered=True, success=True) diff --git a/app/services/payment/cryptobot.py b/app/services/payment/cryptobot.py index f007b5e6..aab5adb5 100644 --- a/app/services/payment/cryptobot.py +++ b/app/services/payment/cryptobot.py @@ -11,8 +11,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import PaymentMethod, TransactionType -from app.services.auto_purchase_service import try_auto_purchase_after_topup -from app.services.user_cart_service import user_cart_service from app.utils.currency_converter import currency_converter from app.utils.user_utils import format_referrer_info @@ -294,25 +292,20 @@ class CryptoBotPaymentMixin: # Проверяем наличие сохраненной корзины для возврата к оформлению подписки try: - autopurchase_result = await try_auto_purchase_after_topup(db, user, getattr(self, "bot", None)) - if autopurchase_result.triggered: - logger.info( - "Автопокупка после пополнения %s для пользователя %s", - "успешна" if autopurchase_result.success else "не выполнена", - user.id, - ) - return True - + from app.services.user_cart_service import user_cart_service from aiogram import types - from app.localization.texts import get_texts - has_saved_cart = await user_cart_service.has_user_cart(user.id) if has_saved_cart and getattr(self, "bot", None): + # Если у пользователя есть сохраненная корзина, + # отправляем ему уведомление с кнопкой вернуться к оформлению + from app.localization.texts import get_texts + texts = get_texts(user.language) cart_message = texts.BALANCE_TOPUP_CART_REMINDER_DETAILED.format( total_amount=settings.format_price(payment.amount_kopeks) ) - + + # Создаем клавиатуру с кнопками keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton( text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT, @@ -327,7 +320,7 @@ class CryptoBotPaymentMixin: callback_data="back_to_menu" )] ]) - + await self.bot.send_message( chat_id=user.telegram_id, text=f"✅ Баланс пополнен на {settings.format_price(payment.amount_kopeks)}!\n\n{cart_message}", diff --git a/app/services/payment/mulenpay.py b/app/services/payment/mulenpay.py index 904b7a27..71a9dd8b 100644 --- a/app/services/payment/mulenpay.py +++ b/app/services/payment/mulenpay.py @@ -11,8 +11,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import PaymentMethod, TransactionType -from app.services.auto_purchase_service import try_auto_purchase_after_topup -from app.services.user_cart_service import user_cart_service from app.utils.user_utils import format_referrer_info logger = logging.getLogger(__name__) @@ -325,19 +323,11 @@ class MulenPayPaymentMixin: # Проверяем наличие сохраненной корзины для возврата к оформлению подписки try: - autopurchase_result = await try_auto_purchase_after_topup(db, user, getattr(self, "bot", None)) - if autopurchase_result.triggered: - logger.info( - "Автопокупка после пополнения %s для пользователя %s", - "успешна" if autopurchase_result.success else "не выполнена", - user.id, - ) - return True - + from app.services.user_cart_service import user_cart_service from aiogram import types has_saved_cart = await user_cart_service.has_user_cart(user.id) if has_saved_cart and getattr(self, "bot", None): - # Если у пользователя есть сохраненная корзина, + # Если у пользователя есть сохраненная корзина, # отправляем ему уведомление с кнопкой вернуться к оформлению from app.localization.texts import get_texts diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index ad2f0b4d..7cae4325 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -12,9 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import PaymentMethod, TransactionType -from app.services.auto_purchase_service import try_auto_purchase_after_topup from app.services.pal24_service import Pal24APIError -from app.services.user_cart_service import user_cart_service from app.utils.user_utils import format_referrer_info logger = logging.getLogger(__name__) @@ -436,15 +434,6 @@ class Pal24PaymentMixin: from aiogram import types has_saved_cart = await user_cart_service.has_user_cart(user.id) - autopurchase_result = await try_auto_purchase_after_topup(db, user, getattr(self, "bot", None)) - if autopurchase_result.triggered: - logger.info( - "Автопокупка после пополнения %s для пользователя %s", - "успешна" if autopurchase_result.success else "не выполнена", - user.id, - ) - return True - if has_saved_cart and getattr(self, "bot", None): from app.localization.texts import get_texts diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py index cc8597f3..ea710427 100644 --- a/app/services/payment/stars.py +++ b/app/services/payment/stars.py @@ -19,8 +19,6 @@ from app.database.crud.transaction import create_transaction from app.database.crud.user import get_user_by_id from app.database.models import PaymentMethod, TransactionType from app.external.telegram_stars import TelegramStarsService -from app.services.auto_purchase_service import try_auto_purchase_after_topup -from app.services.user_cart_service import user_cart_service from app.utils.user_utils import format_referrer_info logger = logging.getLogger(__name__) @@ -241,15 +239,7 @@ class TelegramStarsMixin: # Проверяем наличие сохраненной корзины для возврата к оформлению подписки try: - autopurchase_result = await try_auto_purchase_after_topup(db, user, getattr(self, "bot", None)) - if autopurchase_result.triggered: - logger.info( - "Автопокупка после пополнения %s для пользователя %s", - "успешна" if autopurchase_result.success else "не выполнена", - user.id, - ) - return True - + from app.services.user_cart_service import user_cart_service from aiogram import types has_saved_cart = await user_cart_service.has_user_cart(user.id) if has_saved_cart and getattr(self, "bot", None): diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py index 378e99d3..134c1ca0 100644 --- a/app/services/payment/wata.py +++ b/app/services/payment/wata.py @@ -12,8 +12,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import PaymentMethod, TransactionType -from app.services.auto_purchase_service import try_auto_purchase_after_topup -from app.services.user_cart_service import user_cart_service from app.services.wata_service import WataAPIError, WataService from app.utils.user_utils import format_referrer_info @@ -522,15 +520,7 @@ class WataPaymentMixin: logger.error("Ошибка отправки уведомления пользователю WATA: %s", error) try: - autopurchase_result = await try_auto_purchase_after_topup(db, user, getattr(self, "bot", None)) - if autopurchase_result.triggered: - logger.info( - "Автопокупка после пополнения %s для пользователя %s", - "успешна" if autopurchase_result.success else "не выполнена", - user.id, - ) - return payment - + from app.services.user_cart_service import user_cart_service from aiogram import types has_saved_cart = await user_cart_service.has_user_cart(user.id) diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 67a699b5..b4210899 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -16,8 +16,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import PaymentMethod, TransactionType -from app.services.auto_purchase_service import try_auto_purchase_after_topup -from app.services.user_cart_service import user_cart_service from app.utils.user_utils import format_referrer_info logger = logging.getLogger(__name__) @@ -464,16 +462,8 @@ class YooKassaPaymentMixin: # Проверяем наличие сохраненной корзины для возврата к оформлению подписки # ВАЖНО: этот код должен выполняться даже при ошибках в уведомлениях logger.info(f"Проверяем наличие сохраненной корзины для пользователя {user.id}") + from app.services.user_cart_service import user_cart_service try: - autopurchase_result = await try_auto_purchase_after_topup(db, user, getattr(self, "bot", None)) - if autopurchase_result.triggered: - logger.info( - "Автопокупка после пополнения %s для пользователя %s", - "успешна" if autopurchase_result.success else "не выполнена", - user.id, - ) - return True - has_saved_cart = await user_cart_service.has_user_cart(user.id) logger.info(f"Результат проверки корзины для пользователя {user.id}: {has_saved_cart}") if has_saved_cart and getattr(self, "bot", None): diff --git a/migrations/alembic/versions/f2acb8b40cb5_add_auto_purchase_after_topup.py b/migrations/alembic/versions/f2acb8b40cb5_add_auto_purchase_after_topup.py deleted file mode 100644 index c244ba19..00000000 --- a/migrations/alembic/versions/f2acb8b40cb5_add_auto_purchase_after_topup.py +++ /dev/null @@ -1,36 +0,0 @@ -"""add auto purchase after topup flag""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -revision: str = "f2acb8b40cb5" -down_revision: Union[str, None] = "9f0f2d5a1c7b" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - -USERS_TABLE = "users" -COLUMN_NAME = "auto_purchase_after_topup_enabled" - - -def upgrade() -> None: - op.add_column( - USERS_TABLE, - sa.Column( - COLUMN_NAME, - sa.Boolean(), - nullable=False, - server_default=sa.text("false"), - ), - ) - op.alter_column( - USERS_TABLE, - COLUMN_NAME, - server_default=None, - ) - - -def downgrade() -> None: - op.drop_column(USERS_TABLE, COLUMN_NAME) diff --git a/tests/services/test_payment_service_webhooks.py b/tests/services/test_payment_service_webhooks.py index 1f97c024..f003d765 100644 --- a/tests/services/test_payment_service_webhooks.py +++ b/tests/services/test_payment_service_webhooks.py @@ -762,40 +762,6 @@ async def test_process_pal24_postback_success(monkeypatch: pytest.MonkeyPatch) - ) monkeypatch.setitem(sys.modules, "app.services.user_cart_service", user_cart_stub) - autopurchase_mock = AsyncMock( - return_value=SimpleNamespace(triggered=False, success=False) - ) - monkeypatch.setattr( - "app.services.payment.pal24.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.wata.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.yookassa.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.cryptobot.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.mulenpay.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.stars.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - class DummyTypes: class InlineKeyboardMarkup: def __init__(self, inline_keyboard=None, **kwargs): @@ -962,40 +928,6 @@ async def test_get_pal24_payment_status_auto_finalize(monkeypatch: pytest.Monkey ) monkeypatch.setitem(sys.modules, "app.services.user_cart_service", user_cart_stub) - autopurchase_mock = AsyncMock( - return_value=SimpleNamespace(triggered=False, success=False) - ) - monkeypatch.setattr( - "app.services.payment.pal24.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.wata.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.yookassa.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.cryptobot.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.mulenpay.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - monkeypatch.setattr( - "app.services.payment.stars.try_auto_purchase_after_topup", - autopurchase_mock, - raising=False, - ) - class DummyTypes: class InlineKeyboardMarkup: def __init__(self, inline_keyboard=None, **kwargs):