Merge pull request #2336 from BEDOLAGA-DEV/dev5

Dev5
This commit is contained in:
Egor
2026-01-19 07:53:16 +03:00
committed by GitHub
4 changed files with 695 additions and 11 deletions

View File

@@ -31,6 +31,7 @@ from app.services.subscription_purchase_service import (
PurchaseValidationError,
PurchaseBalanceError,
)
from app.services.user_cart_service import user_cart_service
from ..dependencies import get_cabinet_db, get_current_cabinet_user
from ..schemas.subscription import (
@@ -334,9 +335,60 @@ async def renew_subscription(
# Check balance
if user.balance_kopeks < price_kopeks:
missing = price_kopeks - user.balance_kopeks
# Get tariff info for cart
tariff_id = user.subscription.tariff_id
tariff_name = None
tariff_traffic_limit_gb = None
tariff_device_limit = None
tariff_allowed_squads = None
if tariff_id:
tariff = await get_tariff_by_id(db, tariff_id)
if tariff:
tariff_name = tariff.name
tariff_traffic_limit_gb = tariff.traffic_limit_gb
tariff_device_limit = tariff.device_limit
tariff_allowed_squads = tariff.allowed_squads or []
# Save cart for auto-purchase after balance top-up
cart_data = {
'cart_mode': 'extend',
'subscription_id': user.subscription.id,
'tariff_id': tariff_id,
'period_days': request.period_days,
'total_price': price_kopeks,
'user_id': user.id,
'saved_cart': True,
'missing_amount': missing,
'return_to_cart': True,
'description': f"Продление подписки на {request.period_days} дней" + (f" ({tariff_name})" if tariff_name else ""),
'discount_percent': discount_percent,
'source': 'cabinet',
}
# Add tariff parameters for tariffs mode
if tariff_id:
cart_data['traffic_limit_gb'] = tariff_traffic_limit_gb
cart_data['device_limit'] = tariff_device_limit
cart_data['allowed_squads'] = tariff_allowed_squads
try:
await user_cart_service.save_user_cart(user.id, cart_data)
logger.info(f"Cart saved for auto-renewal (cabinet) user {user.id}")
except Exception as e:
logger.error(f"Error saving cart for auto-renewal (cabinet): {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Insufficient balance. Need {price_kopeks / 100:.2f} RUB, have {user.balance_kopeks / 100:.2f} RUB",
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"code": "insufficient_funds",
"message": f"Недостаточно средств. Не хватает {settings.format_price(missing)}",
"missing_amount": missing,
"cart_saved": True,
"cart_mode": "extend",
},
)
# Deduct balance and extend subscription
@@ -1077,7 +1129,14 @@ async def preview_purchase(
user: User = Depends(get_current_cabinet_user),
db: AsyncSession = Depends(get_cabinet_db),
) -> Dict[str, Any]:
"""Calculate and preview the total price for selected options."""
"""Calculate and preview the total price for selected options (classic mode only)."""
# This endpoint is for classic mode only, tariffs mode uses /purchase-tariff
if settings.is_tariffs_mode():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This endpoint is not available in tariffs mode. Use /purchase-tariff instead.",
)
try:
context = await purchase_service.build_options(db, user)
@@ -1115,7 +1174,14 @@ async def submit_purchase(
user: User = Depends(get_current_cabinet_user),
db: AsyncSession = Depends(get_cabinet_db),
) -> Dict[str, Any]:
"""Submit subscription purchase (deduct from balance)."""
"""Submit subscription purchase (deduct from balance, classic mode only)."""
# This endpoint is for classic mode only, tariffs mode uses /purchase-tariff
if settings.is_tariffs_mode():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This endpoint is not available in tariffs mode. Use /purchase-tariff instead.",
)
try:
context = await purchase_service.build_options(db, user)
@@ -1147,9 +1213,35 @@ async def submit_purchase(
detail=str(e),
)
except PurchaseBalanceError as e:
# Save cart for auto-purchase after balance top-up
try:
total_price = pricing.final_total if 'pricing' in locals() else 0
cart_data = {
'cart_mode': 'subscription_purchase',
'period_id': request.selection.period_id,
'period_days': request.selection.period_days,
'traffic_gb': request.selection.traffic_value, # _prepare_auto_purchase expects traffic_gb
'countries': request.selection.servers, # _prepare_auto_purchase expects countries
'devices': request.selection.devices,
'total_price': total_price,
'user_id': user.id,
'saved_cart': True,
'return_to_cart': True,
'source': 'cabinet',
}
await user_cart_service.save_user_cart(user.id, cart_data)
logger.info(f"Cart saved for auto-purchase (cabinet /purchase) user {user.id}")
except Exception as cart_error:
logger.error(f"Error saving cart for auto-purchase (cabinet /purchase): {cart_error}")
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail=str(e),
detail={
"code": "insufficient_funds",
"message": str(e),
"cart_saved": True,
"cart_mode": "subscription_purchase",
},
)
except Exception as e:
logger.error(f"Failed to submit purchase for user {user.id}: {e}")
@@ -1265,12 +1357,57 @@ async def purchase_tariff(
# Check balance
if user.balance_kopeks < price_kopeks:
missing = price_kopeks - user.balance_kopeks
# Save cart for auto-purchase after balance top-up
if is_daily_tariff:
cart_data = {
'cart_mode': 'daily_tariff_purchase',
'tariff_id': tariff.id,
'is_daily': True,
'daily_price_kopeks': price_kopeks,
'total_price': price_kopeks,
'user_id': user.id,
'saved_cart': True,
'missing_amount': missing,
'return_to_cart': True,
'description': f"Покупка суточного тарифа {tariff.name}",
'traffic_limit_gb': tariff.traffic_limit_gb,
'device_limit': tariff.device_limit,
'allowed_squads': tariff.allowed_squads or [],
'source': 'cabinet',
}
else:
cart_data = {
'cart_mode': 'tariff_purchase',
'tariff_id': tariff.id,
'period_days': period_days,
'total_price': price_kopeks,
'user_id': user.id,
'saved_cart': True,
'missing_amount': missing,
'return_to_cart': True,
'description': f"Покупка тарифа {tariff.name} на {period_days} дней",
'traffic_limit_gb': traffic_limit_gb,
'device_limit': tariff.device_limit,
'allowed_squads': tariff.allowed_squads or [],
'discount_percent': discount_percent,
'source': 'cabinet',
}
try:
await user_cart_service.save_user_cart(user.id, cart_data)
logger.info(f"Cart saved for auto-purchase (cabinet) user {user.id}, tariff {tariff.id}")
except Exception as e:
logger.error(f"Error saving cart for auto-purchase (cabinet): {e}")
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"code": "insufficient_funds",
"message": f"Недостаточно средств. Не хватает {settings.format_price(missing)}",
"missing_amount": missing,
"cart_saved": True,
"cart_mode": cart_data['cart_mode'],
},
)

View File

@@ -621,13 +621,33 @@ async def select_tariff(
)
else:
missing = daily_price - user_balance
# Сохраняем данные корзины для автопокупки суточного тарифа
cart_data = {
'cart_mode': 'daily_tariff_purchase',
'tariff_id': tariff_id,
'is_daily': True,
'daily_price_kopeks': daily_price,
'total_price': daily_price,
'user_id': db_user.id,
'saved_cart': True,
'missing_amount': missing,
'return_to_cart': True,
'description': f"Покупка суточного тарифа {tariff.name}",
'traffic_limit_gb': tariff.traffic_limit_gb,
'device_limit': tariff.device_limit,
'allowed_squads': tariff.allowed_squads or [],
}
await user_cart_service.save_user_cart(db_user.id, cart_data)
await callback.message.edit_text(
f"❌ <b>Недостаточно средств</b>\n\n"
f"📦 Тариф: <b>{tariff.name}</b>\n"
f"🔄 Тип: Суточный\n"
f"💰 Цена: {_format_price_kopeks(daily_price)}/день\n\n"
f"💳 Ваш баланс: {_format_price_kopeks(user_balance)}\n"
f"⚠️ Не хватает: <b>{_format_price_kopeks(missing)}</b>",
f"⚠️ Не хватает: <b>{_format_price_kopeks(missing)}</b>\n\n"
f"🛒 <i>Корзина сохранена! После пополнения баланса подписка будет оформлена автоматически.</i>",
reply_markup=get_daily_tariff_insufficient_balance_keyboard(tariff_id, db_user.language),
parse_mode="HTML"
)
@@ -1087,15 +1107,35 @@ async def select_tariff_period(
parse_mode="HTML"
)
else:
# Недостаточно средств
# Недостаточно средств - сохраняем корзину для автопокупки
missing = final_price - user_balance
# Сохраняем данные корзины для автопокупки после пополнения
cart_data = {
'cart_mode': 'tariff_purchase',
'tariff_id': tariff_id,
'period_days': period,
'total_price': final_price,
'user_id': db_user.id,
'saved_cart': True,
'missing_amount': missing,
'return_to_cart': True,
'description': f"Покупка тарифа {tariff.name} на {period} дней",
'traffic_limit_gb': tariff.traffic_limit_gb,
'device_limit': tariff.device_limit,
'allowed_squads': tariff.allowed_squads or [],
'discount_percent': discount_percent,
}
await user_cart_service.save_user_cart(db_user.id, cart_data)
await callback.message.edit_text(
f"❌ <b>Недостаточно средств</b>\n\n"
f"📦 Тариф: <b>{tariff.name}</b>\n"
f"📅 Период: {_format_period(period)}\n"
f"💰 Стоимость: {_format_price_kopeks(final_price)}\n\n"
f"💳 Ваш баланс: {_format_price_kopeks(user_balance)}\n"
f"⚠️ Не хватает: <b>{_format_price_kopeks(missing)}</b>",
f"⚠️ Не хватает: <b>{_format_price_kopeks(missing)}</b>\n\n"
f"🛒 <i>Корзина сохранена! После пополнения баланса подписка будет оформлена автоматически.</i>",
reply_markup=get_tariff_insufficient_balance_keyboard(tariff_id, period, db_user.language),
parse_mode="HTML"
)
@@ -1584,13 +1624,37 @@ async def select_tariff_extend_period(
)
else:
missing = final_price - user_balance
# Получаем текущую подписку для сохранения в корзину
subscription = await get_subscription_by_user_id(db, db_user.id)
# Сохраняем данные корзины для автопокупки после пополнения
cart_data = {
'cart_mode': 'extend',
'tariff_id': tariff_id,
'subscription_id': subscription.id if subscription else None,
'period_days': period,
'total_price': final_price,
'user_id': db_user.id,
'saved_cart': True,
'missing_amount': missing,
'return_to_cart': True,
'description': f"Продление тарифа {tariff.name} на {period} дней",
'traffic_limit_gb': tariff.traffic_limit_gb,
'device_limit': tariff.device_limit,
'allowed_squads': tariff.allowed_squads or [],
'discount_percent': discount_percent,
}
await user_cart_service.save_user_cart(db_user.id, cart_data)
await callback.message.edit_text(
f"❌ <b>Недостаточно средств</b>\n\n"
f"📦 Тариф: <b>{tariff.name}</b>\n"
f"📅 Период: {_format_period(period)}\n"
f"💰 К оплате: {_format_price_kopeks(final_price)}\n\n"
f"💳 Ваш баланс: {_format_price_kopeks(user_balance)}\n"
f"⚠️ Не хватает: <b>{_format_price_kopeks(missing)}</b>",
f"⚠️ Не хватает: <b>{_format_price_kopeks(missing)}</b>\n\n"
f"🛒 <i>Корзина сохранена! После пополнения баланса подписка будет продлена автоматически.</i>",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")],
[InlineKeyboardButton(text=texts.BACK, callback_data="subscription_extend")]

View File

@@ -260,7 +260,9 @@ class CloudPaymentsPaymentMixin:
# Auto-purchase if enabled
auto_purchase_success = False
try:
auto_purchase_success = await auto_purchase_saved_cart_after_topup(db, user)
auto_purchase_success = await auto_purchase_saved_cart_after_topup(
db, user, bot=getattr(self, "bot", None)
)
except Exception as error:
logger.exception("Ошибка автопокупки после CloudPayments: %s", error)

View File

@@ -56,6 +56,8 @@ class AutoExtendContext:
traffic_limit_gb: Optional[int] = None
squad_uuid: Optional[str] = None
consume_promo_offer: bool = False
tariff_id: Optional[int] = None
allowed_squads: Optional[list] = None
async def _prepare_auto_purchase(
@@ -273,6 +275,7 @@ async def _prepare_auto_extend_context(
squad_uuid = cart_data.get("squad_uuid")
consume_promo_offer = bool(cart_data.get("consume_promo_offer"))
allowed_squads = cart_data.get("allowed_squads")
return AutoExtendContext(
subscription=subscription,
@@ -283,16 +286,26 @@ async def _prepare_auto_extend_context(
traffic_limit_gb=traffic_limit_gb,
squad_uuid=squad_uuid,
consume_promo_offer=consume_promo_offer,
tariff_id=tariff_id,
allowed_squads=allowed_squads,
)
def _apply_extension_updates(context: AutoExtendContext) -> None:
"""
Применяет обновления лимитов подписки (трафик, устройства, серверы).
Применяет обновления лимитов подписки (трафик, устройства, серверы, тариф).
НЕ изменяет is_trial - это делается позже после успешного коммита продления.
"""
subscription = context.subscription
# Обновляем tariff_id если указан в контексте
if context.tariff_id is not None:
subscription.tariff_id = context.tariff_id
# Обновляем allowed_squads если указаны (заменяем полностью)
if context.allowed_squads is not None:
subscription.connected_squads = context.allowed_squads
# Обновляем лимиты для триальной подписки
if subscription.is_trial:
# НЕ удаляем триал здесь! Это будет сделано после успешного extend_subscription()
@@ -528,6 +541,464 @@ async def _auto_extend_subscription(
return True
async def _auto_purchase_tariff(
db: AsyncSession,
user: User,
cart_data: dict,
*,
bot: Optional[Bot] = None,
) -> bool:
"""Автоматическая покупка периодного тарифа из сохранённой корзины."""
from datetime import datetime
from app.database.crud.tariff import get_tariff_by_id
from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id, extend_subscription
from app.database.crud.transaction import create_transaction
from app.database.crud.user import subtract_user_balance
from app.database.crud.server_squad import get_all_server_squads
from app.database.models import TransactionType
tariff_id = _safe_int(cart_data.get("tariff_id"))
period_days = _safe_int(cart_data.get("period_days"))
discount_percent = _safe_int(cart_data.get("discount_percent"))
if not tariff_id or period_days <= 0:
logger.warning(
"🔁 Автопокупка тарифа: некорректные данные корзины для пользователя %s (tariff_id=%s, period=%s)",
user.telegram_id,
tariff_id,
period_days,
)
return False
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff or not tariff.is_active:
logger.warning(
"🔁 Автопокупка тарифа: тариф %s недоступен для пользователя %s",
tariff_id,
user.telegram_id,
)
return False
# Получаем актуальную цену тарифа
prices = tariff.period_prices or {}
base_price = prices.get(str(period_days))
if base_price is None:
logger.warning(
"🔁 Автопокупка тарифа: период %s дней недоступен для тарифа %s",
period_days,
tariff_id,
)
return False
final_price = _apply_promo_discount_for_tariff(base_price, discount_percent)
if user.balance_kopeks < final_price:
logger.info(
"🔁 Автопокупка тарифа: у пользователя %s недостаточно средств (%s < %s)",
user.telegram_id,
user.balance_kopeks,
final_price,
)
return False
# Списываем баланс
try:
description = f"Покупка тарифа {tariff.name} на {period_days} дней"
success = await subtract_user_balance(db, user, final_price, description)
if not success:
logger.warning(
"❌ Автопокупка тарифа: не удалось списать баланс пользователя %s",
user.telegram_id,
)
return False
except Exception as error:
logger.error(
"❌ Автопокупка тарифа: ошибка списания баланса пользователя %s: %s",
user.telegram_id,
error,
exc_info=True,
)
return False
# Получаем список серверов из тарифа
squads = tariff.allowed_squads or []
if not squads:
all_servers, _ = await get_all_server_squads(db, available_only=True)
squads = [s.squad_uuid for s in all_servers if s.squad_uuid]
# Проверяем есть ли уже подписка
existing_subscription = await get_subscription_by_user_id(db, user.id)
try:
if existing_subscription:
# Продлеваем существующую подписку
subscription = await extend_subscription(
db,
existing_subscription,
days=period_days,
tariff_id=tariff.id,
traffic_limit_gb=tariff.traffic_limit_gb,
device_limit=tariff.device_limit,
connected_squads=squads,
)
was_trial_conversion = existing_subscription.is_trial
if was_trial_conversion:
subscription.is_trial = False
subscription.status = "active"
user.has_had_paid_subscription = True
await db.commit()
else:
# Создаём новую подписку
subscription = await create_paid_subscription(
db=db,
user_id=user.id,
duration_days=period_days,
traffic_limit_gb=tariff.traffic_limit_gb,
device_limit=tariff.device_limit,
connected_squads=squads,
tariff_id=tariff.id,
)
was_trial_conversion = False
except Exception as error:
logger.error(
"❌ Автопокупка тарифа: ошибка создания подписки для пользователя %s: %s",
user.telegram_id,
error,
exc_info=True,
)
await db.rollback()
return False
# Создаём транзакцию
try:
transaction = await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=final_price,
description=description,
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка тарифа: не удалось создать транзакцию для пользователя %s: %s",
user.telegram_id,
error,
)
transaction = None
# Обновляем Remnawave
try:
subscription_service = SubscriptionService()
await subscription_service.create_remnawave_user(
db,
subscription,
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
reset_reason="покупка тарифа",
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка тарифа: не удалось обновить Remnawave для пользователя %s: %s",
user.telegram_id,
error,
)
# Очищаем корзину
await user_cart_service.delete_user_cart(user.id)
await clear_subscription_checkout_draft(user.id)
# Уведомления
if bot:
texts = get_texts(getattr(user, "language", "ru"))
period_label = format_period_description(period_days, getattr(user, "language", "ru"))
try:
notification_service = AdminNotificationService(bot)
await notification_service.send_subscription_purchase_notification(
db, user, subscription, transaction, period_days, was_trial_conversion
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка тарифа: не удалось уведомить админов о покупке пользователя %s: %s",
user.telegram_id,
error,
)
try:
message = texts.t(
"AUTO_PURCHASE_SUBSCRIPTION_SUCCESS",
"✅ Подписка на {period} автоматически оформлена после пополнения баланса.",
).format(period=period_label)
hint = texts.t(
"AUTO_PURCHASE_SUBSCRIPTION_HINT",
"Перейдите в раздел «Моя подписка», чтобы получить ссылку.",
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(
text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"),
callback_data="menu_subscription",
)],
[InlineKeyboardButton(
text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "🏠 Главное меню"),
callback_data="back_to_menu",
)],
]
)
await bot.send_message(
chat_id=user.telegram_id,
text=f"{message}\n\n{hint}",
reply_markup=keyboard,
parse_mode="HTML",
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка тарифа: не удалось уведомить пользователя %s: %s",
user.telegram_id,
error,
)
logger.info(
"✅ Автопокупка тарифа: подписка на тариф %s (%s дней) оформлена для пользователя %s",
tariff.name,
period_days,
user.telegram_id,
)
return True
async def _auto_purchase_daily_tariff(
db: AsyncSession,
user: User,
cart_data: dict,
*,
bot: Optional[Bot] = None,
) -> bool:
"""Автоматическая покупка суточного тарифа из сохранённой корзины."""
from datetime import datetime, timedelta
from app.database.crud.tariff import get_tariff_by_id
from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id
from app.database.crud.transaction import create_transaction
from app.database.crud.user import subtract_user_balance
from app.database.crud.server_squad import get_all_server_squads
from app.database.models import TransactionType
tariff_id = _safe_int(cart_data.get("tariff_id"))
if not tariff_id:
logger.warning(
"🔁 Автопокупка суточного тарифа: нет tariff_id в корзине пользователя %s",
user.telegram_id,
)
return False
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff or not tariff.is_active:
logger.warning(
"🔁 Автопокупка суточного тарифа: тариф %s недоступен для пользователя %s",
tariff_id,
user.telegram_id,
)
return False
if not getattr(tariff, 'is_daily', False):
logger.warning(
"🔁 Автопокупка суточного тарифа: тариф %s не является суточным для пользователя %s",
tariff_id,
user.telegram_id,
)
return False
daily_price = getattr(tariff, 'daily_price_kopeks', 0)
if daily_price <= 0:
logger.warning(
"🔁 Автопокупка суточного тарифа: некорректная цена тарифа %s для пользователя %s",
tariff_id,
user.telegram_id,
)
return False
if user.balance_kopeks < daily_price:
logger.info(
"🔁 Автопокупка суточного тарифа: у пользователя %s недостаточно средств (%s < %s)",
user.telegram_id,
user.balance_kopeks,
daily_price,
)
return False
# Списываем баланс за первый день
try:
description = f"Активация суточного тарифа {tariff.name}"
success = await subtract_user_balance(db, user, daily_price, description)
if not success:
logger.warning(
"❌ Автопокупка суточного тарифа: не удалось списать баланс пользователя %s",
user.telegram_id,
)
return False
except Exception as error:
logger.error(
"❌ Автопокупка суточного тарифа: ошибка списания баланса пользователя %s: %s",
user.telegram_id,
error,
exc_info=True,
)
return False
# Получаем список серверов из тарифа
squads = tariff.allowed_squads or []
if not squads:
all_servers, _ = await get_all_server_squads(db, available_only=True)
squads = [s.squad_uuid for s in all_servers if s.squad_uuid]
# Проверяем есть ли уже подписка
existing_subscription = await get_subscription_by_user_id(db, user.id)
try:
if existing_subscription:
# Обновляем существующую подписку на суточный тариф
# Суточность определяется через tariff.is_daily, поэтому достаточно установить tariff_id
was_trial_conversion = existing_subscription.is_trial # Сохраняем до изменения
existing_subscription.tariff_id = tariff.id
existing_subscription.traffic_limit_gb = tariff.traffic_limit_gb
existing_subscription.device_limit = tariff.device_limit
existing_subscription.connected_squads = squads
existing_subscription.status = "active"
existing_subscription.is_trial = False
existing_subscription.last_daily_charge_at = datetime.utcnow()
existing_subscription.is_daily_paused = False
existing_subscription.end_date = datetime.utcnow() + timedelta(days=1)
if was_trial_conversion:
user.has_had_paid_subscription = True
await db.commit()
await db.refresh(existing_subscription)
subscription = existing_subscription
else:
# Создаём новую суточную подписку
# Суточность определяется через tariff.is_daily
subscription = await create_paid_subscription(
db=db,
user_id=user.id,
duration_days=1,
traffic_limit_gb=tariff.traffic_limit_gb,
device_limit=tariff.device_limit,
connected_squads=squads,
tariff_id=tariff.id,
)
# Устанавливаем параметры для суточного списания
subscription.last_daily_charge_at = datetime.utcnow()
subscription.is_daily_paused = False
await db.commit()
was_trial_conversion = False
except Exception as error:
logger.error(
"❌ Автопокупка суточного тарифа: ошибка создания подписки для пользователя %s: %s",
user.telegram_id,
error,
exc_info=True,
)
await db.rollback()
return False
# Создаём транзакцию
try:
transaction = await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=daily_price,
description=description,
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка суточного тарифа: не удалось создать транзакцию для пользователя %s: %s",
user.telegram_id,
error,
)
transaction = None
# Обновляем Remnawave
try:
subscription_service = SubscriptionService()
await subscription_service.create_remnawave_user(
db,
subscription,
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
reset_reason="активация суточного тарифа",
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка суточного тарифа: не удалось обновить Remnawave для пользователя %s: %s",
user.telegram_id,
error,
)
# Очищаем корзину
await user_cart_service.delete_user_cart(user.id)
await clear_subscription_checkout_draft(user.id)
# Уведомления
if bot:
texts = get_texts(getattr(user, "language", "ru"))
try:
notification_service = AdminNotificationService(bot)
await notification_service.send_subscription_purchase_notification(
db, user, subscription, transaction, 1, was_trial_conversion
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка суточного тарифа: не удалось уведомить админов о покупке пользователя %s: %s",
user.telegram_id,
error,
)
try:
message = (
f"✅ <b>Суточный тариф «{tariff.name}» активирован!</b>\n\n"
f"💰 Списано: {daily_price / 100:.0f} ₽ за первый день\n"
f"🔄 Средства будут списываться автоматически раз в сутки.\n\n"
f" Вы можете приостановить подписку в любой момент."
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(
text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"),
callback_data="menu_subscription",
)],
[InlineKeyboardButton(
text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "🏠 Главное меню"),
callback_data="back_to_menu",
)],
]
)
await bot.send_message(
chat_id=user.telegram_id,
text=message,
reply_markup=keyboard,
parse_mode="HTML",
)
except Exception as error:
logger.warning(
"⚠️ Автопокупка суточного тарифа: не удалось уведомить пользователя %s: %s",
user.telegram_id,
error,
)
logger.info(
"✅ Автопокупка суточного тарифа: тариф %s активирован для пользователя %s",
tariff.name,
user.telegram_id,
)
return True
async def auto_purchase_saved_cart_after_topup(
db: AsyncSession,
user: User,
@@ -551,9 +1022,19 @@ async def auto_purchase_saved_cart_after_topup(
)
cart_mode = cart_data.get("cart_mode") or cart_data.get("mode")
# Обработка продления подписки
if cart_mode == "extend":
return await _auto_extend_subscription(db, user, cart_data, bot=bot)
# Обработка покупки периодного тарифа
if cart_mode == "tariff_purchase":
return await _auto_purchase_tariff(db, user, cart_data, bot=bot)
# Обработка покупки суточного тарифа
if cart_mode == "daily_tariff_purchase":
return await _auto_purchase_daily_tariff(db, user, cart_data, bot=bot)
try:
prepared = await _prepare_auto_purchase(db, user, cart_data)
except PurchaseValidationError as error: