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):