Revert "Add automatic subscription purchase after top-ups"

This commit is contained in:
Egor
2025-10-26 17:39:04 +03:00
committed by GitHub
parent 3a730492cb
commit 2e1dfa6638
14 changed files with 24 additions and 733 deletions

View File

@@ -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")

View File

@@ -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"

View File

@@ -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(

View File

@@ -780,16 +780,6 @@
"BALANCE_SUPPORT_REQUEST": "🛠️ Request via support",
"BALANCE_TOPUP": "💳 Top up balance",
"BALANCE_TOPUP_CART_REMINDER_DETAILED": "\\n💡 <b>Balance top-up required</b>\\n\\nYour cart contains items totaling {total_amount}, but your current balance is insufficient.\\n\\n💳 <b>Top up your balance</b> to complete the purchase.\\n\\nChoose a top-up method:",
"AUTO_PURCHASE_AFTER_TOPUP_STATUS": "🤖 <b>Auto purchase after top-up:</b> {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": "🤖 <b>Auto purchase completed</b>\\n\\nYour subscription for {period} was paid automatically. Charged: {amount}.",
"AUTO_PURCHASE_AFTER_TOPUP_SUCCESS_WITH_DISCOUNT": "🤖 <b>Auto purchase completed</b>\\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",

View File

@@ -780,16 +780,6 @@
"BALANCE_SUPPORT_REQUEST": "🛠️ Запрос через поддержку",
"BALANCE_TOPUP": "💳 Пополнить баланс",
"BALANCE_TOPUP_CART_REMINDER_DETAILED": "\n💡 <b>Требуется пополнение баланса</b>\n\nВ вашей корзине находятся товары на общую сумму {total_amount}, но на балансе недостаточно средств.\n\n💳 <b>Пополните баланс</b>, чтобы завершить покупку.\n\nВыберите способ пополнения:",
"AUTO_PURCHASE_AFTER_TOPUP_STATUS": "🤖 <b>Автопокупка после пополнения:</b> {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": "🤖 <b>Автопокупка выполнена</b>\n\nПодписка на {period} оплачена автоматически. Списано: {amount}.",
"AUTO_PURCHASE_AFTER_TOPUP_SUCCESS_WITH_DISCOUNT": "🤖 <b>Автопокупка выполнена</b>\n\nПодписка на {period} оплачена автоматически. Списано: {amount}. Скидка: {discount}.",
"AUTO_PURCHASE_AFTER_TOPUP_INSUFFICIENT": "🤖 Автопокупка не выполнена — на балансе все еще недостаточно средств.",
"BALANCE_TOP_UP": "💳 Пополнить",
"BLOCK_BY_TIME": "⏳ Блокировка по времени",
"BLOCK_FOREVER": "🚫 Заблокировать",

View File

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

View File

@@ -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}",

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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