mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Revert "Add automatic subscription purchase after top-ups"
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "🚫 Заблокировать",
|
||||
|
||||
@@ -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)
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user