mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
@@ -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'],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user