This commit is contained in:
gy9vin
2026-01-30 23:43:29 +03:00
9 changed files with 410 additions and 237 deletions
+71 -172
View File
@@ -13,6 +13,7 @@ from app.config import settings
from app.database.crud.user import get_user_by_id
from app.database.models import PaymentMethod, Transaction, User
from app.external.cryptobot import CryptoBotService
from app.services.payment_method_config_service import get_enabled_methods_for_user
from app.services.payment_service import PaymentService
from app.services.payment_verification_service import (
SUPPORTED_MANUAL_CHECK_METHODS,
@@ -128,185 +129,83 @@ async def get_transactions(
@router.get('/payment-methods', response_model=list[PaymentMethodResponse])
async def get_payment_methods():
"""Get available payment methods."""
async def get_payment_methods(
user: User = Depends(get_current_cabinet_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get available payment methods for the current user.
Uses PaymentMethodConfig from database for:
- Sort order (sort_order)
- Enabled/disabled status (is_enabled)
- Display names (display_name with fallback to env)
- Min/max amounts (with fallback to env defaults)
- Sub-options filtering (sub_options)
- User filters (user_type_filter, first_topup_filter, promo_group_filter)
"""
# Check if this is user's first topup
from sqlalchemy import exists
has_completed_topup = await db.execute(
select(
exists().where(
Transaction.user_id == user.id,
Transaction.type == 'deposit',
Transaction.is_completed == True,
)
)
)
is_first_topup = not has_completed_topup.scalar()
# Get enabled methods from database config
enabled_methods = await get_enabled_methods_for_user(db, user=user, is_first_topup=is_first_topup)
# Build response with additional options formatting
methods = []
for method_data in enabled_methods:
method_id = method_data['id']
# YooKassa - with card and SBP options
if settings.is_yookassa_enabled():
methods.append(
PaymentMethodResponse(
id='yookassa',
name=settings.get_yookassa_display_name(),
description='Pay via YooKassa',
min_amount_kopeks=settings.YOOKASSA_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.YOOKASSA_MAX_AMOUNT_KOPEKS,
is_available=True,
options=[
{'id': 'card', 'name': '💳 Карта', 'description': 'Банковская карта'},
{'id': 'sbp', 'name': '🏦 СБП', 'description': 'Система быстрых платежей (QR)'},
],
)
)
# Format options with descriptions for specific methods
options = method_data.get('options')
if options:
formatted_options = []
for opt in options:
opt_id = opt['id']
opt_name = opt.get('name', opt_id)
description = ''
# CryptoBot
if settings.is_cryptobot_enabled():
methods.append(
PaymentMethodResponse(
id='cryptobot',
name=settings.get_cryptobot_display_name(),
description='Pay with cryptocurrency via CryptoBot',
min_amount_kopeks=1000,
max_amount_kopeks=10000000,
is_available=True,
)
)
# Add descriptions based on method and option
if method_id in ('yookassa', 'pal24', 'cloudpayments', 'freekassa'):
if opt_id == 'card':
opt_name = f'💳 {opt_name}'
description = 'Банковская карта'
elif opt_id == 'sbp':
opt_name = f'🏦 {opt_name}'
description = 'Система быстрых платежей'
elif method_id == 'platega':
# Platega options already have descriptions from config
definitions = settings.get_platega_method_definitions()
info = definitions.get(int(opt_id), {}) if opt_id.isdigit() else {}
description = info.get('description') or info.get('name') or ''
# Telegram Stars
if settings.TELEGRAM_STARS_ENABLED:
methods.append(
PaymentMethodResponse(
id='telegram_stars',
name=settings.get_telegram_stars_display_name(),
description='Pay with Telegram Stars',
min_amount_kopeks=100,
max_amount_kopeks=1000000,
is_available=True,
)
)
# Heleket
if settings.is_heleket_enabled():
methods.append(
PaymentMethodResponse(
id='heleket',
name=settings.get_heleket_display_name(),
description='Pay with cryptocurrency via Heleket',
min_amount_kopeks=1000,
max_amount_kopeks=10000000,
is_available=True,
)
)
# MulenPay
if settings.is_mulenpay_enabled():
methods.append(
PaymentMethodResponse(
id='mulenpay',
name=settings.get_mulenpay_display_name(),
description='MulenPay payment',
min_amount_kopeks=settings.MULENPAY_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.MULENPAY_MAX_AMOUNT_KOPEKS,
is_available=True,
)
)
# PAL24 - add options for card/sbp
if settings.is_pal24_enabled():
methods.append(
PaymentMethodResponse(
id='pal24',
name=settings.get_pal24_display_name(),
description='Pay via PAL24',
min_amount_kopeks=settings.PAL24_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.PAL24_MAX_AMOUNT_KOPEKS,
is_available=True,
options=[
{'id': 'sbp', 'name': '🏦 СБП', 'description': 'Система быстрых платежей'},
{'id': 'card', 'name': '💳 Карта', 'description': 'Банковская карта'},
],
)
)
# Platega - add options for different payment methods
if settings.is_platega_enabled():
platega_methods = settings.get_platega_active_methods()
definitions = settings.get_platega_method_definitions()
platega_options = []
for method_code in platega_methods:
info = definitions.get(method_code, {})
platega_options.append(
{
'id': str(method_code),
'name': info.get('title') or info.get('name') or f'Platega {method_code}',
'description': info.get('description') or info.get('name') or '',
}
)
formatted_options.append(
{
'id': opt_id,
'name': opt_name,
'description': description,
}
)
options = formatted_options if formatted_options else None
methods.append(
PaymentMethodResponse(
id='platega',
name=settings.get_platega_display_name(),
description='Pay via Platega',
min_amount_kopeks=settings.PLATEGA_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.PLATEGA_MAX_AMOUNT_KOPEKS,
is_available=True,
options=platega_options if platega_options else None,
)
)
# Wata
if settings.is_wata_enabled():
methods.append(
PaymentMethodResponse(
id='wata',
name=settings.get_wata_display_name(),
description='Pay via Wata',
min_amount_kopeks=settings.WATA_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.WATA_MAX_AMOUNT_KOPEKS,
is_available=True,
)
)
# CloudPayments
if settings.is_cloudpayments_enabled():
methods.append(
PaymentMethodResponse(
id='cloudpayments',
name=settings.get_cloudpayments_display_name(),
description='Pay with bank card via CloudPayments',
min_amount_kopeks=settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS,
is_available=True,
)
)
# FreeKassa
if settings.is_freekassa_enabled():
methods.append(
PaymentMethodResponse(
id='freekassa',
name=settings.get_freekassa_display_name(),
description='Pay via FreeKassa',
min_amount_kopeks=settings.FREEKASSA_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.FREEKASSA_MAX_AMOUNT_KOPEKS,
is_available=True,
)
)
# KassaAI
if settings.is_kassa_ai_enabled():
methods.append(
PaymentMethodResponse(
id='kassa_ai',
name=settings.get_kassa_ai_display_name(),
description='Pay via KassaAI',
min_amount_kopeks=settings.KASSA_AI_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.KASSA_AI_MAX_AMOUNT_KOPEKS,
is_available=True,
)
)
# Tribute
if settings.TRIBUTE_ENABLED and settings.TRIBUTE_DONATE_LINK:
methods.append(
PaymentMethodResponse(
id='tribute',
name='Tribute',
description='Pay with bank card via Tribute',
min_amount_kopeks=10000,
max_amount_kopeks=10000000,
id=method_id,
name=method_data['name'],
description=None,
min_amount_kopeks=method_data['min_amount_kopeks'],
max_amount_kopeks=method_data['max_amount_kopeks'],
is_available=True,
options=options,
)
)
@@ -414,7 +313,7 @@ async def create_topup(
):
"""Create payment for balance top-up."""
# Validate payment method
methods = await get_payment_methods()
methods = await get_payment_methods(user=user, db=db)
method = next((m for m in methods if m.id == request.payment_method), None)
if not method or not method.is_available:
-2
View File
@@ -1667,8 +1667,6 @@ async def purchase_tariff(
# Save cart for auto-renewal (not for daily tariffs - they have their own charging)
if not is_daily_tariff:
try:
from app.services.user_cart_service import user_cart_service
cart_data = {
'cart_mode': 'extend',
'subscription_id': subscription.id,
+1 -57
View File
@@ -1193,57 +1193,7 @@ class MonitoringService:
try:
get_texts(user.language)
# Рассчитываем минимальную цену за подписку с минимальной конфигурацией
from app.config import PERIOD_PRICES, settings
from app.utils.pricing_utils import apply_percentage_discount
# Базовая цена за 30 дней
base_price_original = PERIOD_PRICES.get(30, settings.PRICE_30_DAYS)
# Применяем скидку промогруппы для категории "period"
promo_group_discount = user.get_promo_discount('period', 30) if user else 0
# Применяем пользовательскую промо-скидку (если есть)
user_discount_percent = self._get_user_promo_offer_discount_percent(user)
# Общая скидка - максимальная из промогруппы и пользовательской
total_discount_percent = max(promo_group_discount, user_discount_percent)
base_price, _ = apply_percentage_discount(base_price_original, total_discount_percent)
# Добавляем цену за трафик (если фиксированный трафик включён)
if settings.is_traffic_fixed():
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
# Применяем скидки на трафик
traffic_discount = user.get_promo_discount('traffic', 30) if user else 0
traffic_price, _ = apply_percentage_discount(traffic_price, traffic_discount)
else:
traffic_price = 0 # Трафик не фиксирован, цена включена в базовую
# Добавляем цену за серверы (предполагаем минимум 1 сервер по минимальной цене)
# Вместо сложного запроса к БД, используем настройки
# Для минимальной конфигурации - один сервер с минимальной ценой
min_server_price = getattr(settings, 'MIN_SERVER_PRICE', 0) or 0
if min_server_price == 0:
# Если нет явной минимальной цены, используем базовую цену
# В реальных условиях цена сервера будет определяться в ходе оформления подписки
min_server_price = 0
# Добавляем цену за устройства (если больше базового лимита)
# В минимальной конфигурации - базовый лимит, без доп. устройств
device_limit = settings.DEFAULT_DEVICE_LIMIT
additional_devices = max(0, device_limit - settings.DEFAULT_DEVICE_LIMIT)
additional_devices * settings.PRICE_PER_DEVICE
# Для простоты и правильной работы без обращения к БД, рассчитываем минимальную цену как:
# базовая цена + минимальная цена за трафик (если есть фиксированный)
min_server_price = 0 # для минимальной конфигурации с 1 сервером используем 0 или минимальную известную
# Попробуем получить минимальную цену сервера из настроек или используем подходящее значение
# Находим минимальную возможную цену из возможных цен серверов
# В упрощенном варианте используем базовую конфигурацию: базовая цена + трафик
min_total_price = base_price + traffic_price
message = f"""
message = """
🎁 <b>Тестовая подписка скоро закончится!</b>
Ваша тестовая подписка истекает через 2 часа.
@@ -1251,12 +1201,6 @@ class MonitoringService:
💎 <b>Не хотите остаться без VPN?</b>
Переходите на полную подписку!
🔥 <b>Специальное предложение:</b>
30 дней всего за {settings.format_price(min_total_price)}
Безлимитный трафик
Все серверы доступны
Скорость до 1ГБит/сек
Успейте оформить до окончания тестового периода!
"""
+22
View File
@@ -132,6 +132,28 @@ class PaymentCommonMixin:
payment_method_title: str | None = None,
) -> None:
"""Отправляет пользователю уведомление об успешном платеже."""
# Lazy import to avoid circular dependency
from app.cabinet.routes.websocket import notify_user_balance_topup
# Send WebSocket notification to cabinet frontend (works for both Telegram and email-only users)
user_id = getattr(user, 'id', None) if user else None
if user_id:
try:
# Get new balance from user
new_balance = getattr(user, 'balance_kopeks', 0)
await notify_user_balance_topup(
user_id=user_id,
amount_kopeks=amount_kopeks,
new_balance_kopeks=new_balance,
description=payment_method_title or '',
)
except Exception as ws_error:
logger.warning(
'Не удалось отправить WS уведомление о пополнении баланса для user_id=%s: %s',
user_id,
ws_error,
)
if not getattr(self, 'bot', None):
# Если бот не передан (например, внутри фоновых задач), уведомление пропускаем.
return
+52
View File
@@ -12,6 +12,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
from app.utils.user_utils import format_referrer_info
@@ -424,6 +428,54 @@ class HeleketPaymentMixin:
else:
logger.info(f'Пропуск Telegram-уведомления Heleket для email-пользователя {user.id}')
# Автопокупка из сохранённой корзины и умная автоактивация
try:
from app.services.user_cart_service import user_cart_service
has_saved_cart = await user_cart_service.has_user_cart(user.id)
auto_purchase_success = False
if has_saved_cart:
try:
auto_purchase_success = await auto_purchase_saved_cart_after_topup(
db,
user,
bot=getattr(self, 'bot', None),
)
except Exception as auto_error:
logger.error(
'Ошибка автоматической покупки подписки для пользователя %s: %s',
user.id,
auto_error,
exc_info=True,
)
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
if not auto_purchase_success:
try:
await auto_activate_subscription_after_topup(
db,
user,
bot=getattr(self, 'bot', None),
topup_amount=amount_kopeks,
)
except Exception as auto_activate_error:
logger.error(
'Ошибка умной автоактивации для пользователя %s: %s',
user.id,
auto_activate_error,
exc_info=True,
)
except Exception as error:
logger.error(
'Ошибка при работе с автоактивацией для пользователя %s: %s',
user.id,
error,
exc_info=True,
)
return updated_payment
async def process_heleket_webhook(
@@ -280,3 +280,108 @@ async def get_all_promo_groups(db: AsyncSession) -> list[PromoGroup]:
"""Get all promo groups for the filter selector."""
result = await db.execute(select(PromoGroup).order_by(PromoGroup.priority.desc(), PromoGroup.name))
return list(result.scalars().all())
# ============ User-facing methods ============
async def get_enabled_methods_for_user(
db: AsyncSession,
user: 'User | None' = None,
is_first_topup: bool | None = None,
) -> list[dict]:
"""Get payment methods available for a specific user.
Applies all filters from PaymentMethodConfig:
- is_enabled
- is_provider_configured (from env)
- user_type_filter
- first_topup_filter
- promo_group_filter
Returns list of dicts with method info ready for API response.
"""
from app.database.models import UserPromoGroup
configs = await get_all_configs(db)
defaults = _get_method_defaults()
result = []
for config in configs:
method_id = config.method_id
method_def = defaults.get(method_id, {})
# Skip if not enabled in admin panel
if not config.is_enabled:
continue
# Skip if provider not configured in env
if not method_def.get('is_configured', False):
continue
# Apply user_type_filter
if user and config.user_type_filter != 'all':
if config.user_type_filter == 'telegram' and not user.telegram_id:
continue
if config.user_type_filter == 'email' and not getattr(user, 'email', None):
continue
# Apply first_topup_filter
if config.first_topup_filter != 'any' and is_first_topup is not None:
if config.first_topup_filter == 'yes' and not is_first_topup:
continue
if config.first_topup_filter == 'no' and is_first_topup:
continue
# Apply promo_group_filter
if config.promo_group_filter_mode == 'selected' and user:
allowed_group_ids = {pg.id for pg in config.allowed_promo_groups}
if allowed_group_ids:
# Get user's promo groups
user_groups_result = await db.execute(
select(UserPromoGroup.promo_group_id).where(UserPromoGroup.user_id == user.id)
)
user_group_ids = set(user_groups_result.scalars().all())
# Check if user has at least one allowed group
if not user_group_ids.intersection(allowed_group_ids):
continue
# Build display name
display_name = config.display_name or method_def.get('default_display_name', method_id)
# Build min/max amounts (DB overrides env defaults)
min_amount = (
config.min_amount_kopeks if config.min_amount_kopeks is not None else method_def.get('default_min', 1000)
)
max_amount = (
config.max_amount_kopeks
if config.max_amount_kopeks is not None
else method_def.get('default_max', 10000000)
)
# Build options (filter by sub_options config)
options = None
available_sub_options = method_def.get('available_sub_options')
if available_sub_options and config.sub_options:
enabled_options = []
for opt in available_sub_options:
opt_id = opt['id']
if config.sub_options.get(opt_id, True):
enabled_options.append(opt)
if enabled_options:
options = enabled_options
result.append(
{
'id': method_id,
'name': display_name,
'min_amount_kopeks': min_amount,
'max_amount_kopeks': max_amount,
'options': options,
'sort_order': config.sort_order,
}
)
return result
@@ -347,6 +347,9 @@ async def _auto_extend_subscription(
*,
bot: Bot | None = None,
) -> bool:
# Lazy import to avoid circular dependency
from app.cabinet.routes.websocket import notify_user_subscription_renewed
try:
prepared = await _prepare_auto_extend_context(db, user, cart_data)
except Exception as error: # pragma: no cover - defensive logging
@@ -559,6 +562,20 @@ async def _auto_extend_subscription(
_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
try:
await notify_user_subscription_renewed(
user_id=user.id,
new_expires_at=new_end_date.isoformat() if new_end_date else '',
amount_kopeks=prepared.price_kopeks,
)
except Exception as ws_error:
logger.warning(
'⚠️ Автопокупка: не удалось отправить WS уведомление о продлении для %s: %s',
_format_user_id(user),
ws_error,
)
return True
@@ -570,6 +587,11 @@ async def _auto_purchase_tariff(
bot: Bot | None = None,
) -> bool:
"""Автоматическая покупка периодного тарифа из сохранённой корзины."""
# Lazy imports to avoid circular dependency
from app.cabinet.routes.websocket import (
notify_user_subscription_activated,
notify_user_subscription_renewed,
)
from app.database.crud.server_squad import get_all_server_squads
from app.database.crud.subscription import (
create_paid_subscription,
@@ -814,6 +836,29 @@ async def _auto_purchase_tariff(
_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
try:
if existing_subscription:
# Renewal of existing subscription
await notify_user_subscription_renewed(
user_id=user.id,
new_expires_at=subscription.end_date.isoformat() if subscription.end_date else '',
amount_kopeks=final_price,
)
else:
# New subscription activation
await notify_user_subscription_activated(
user_id=user.id,
expires_at=subscription.end_date.isoformat() if subscription.end_date else '',
tariff_name=tariff.name,
)
except Exception as ws_error:
logger.warning(
'⚠️ Автопокупка тарифа: не удалось отправить WS уведомление для %s: %s',
_format_user_id(user),
ws_error,
)
return True
@@ -827,6 +872,11 @@ async def _auto_purchase_daily_tariff(
"""Автоматическая покупка суточного тарифа из сохранённой корзины."""
from datetime import datetime, timedelta
# Lazy imports to avoid circular dependency
from app.cabinet.routes.websocket import (
notify_user_subscription_activated,
notify_user_subscription_renewed,
)
from app.database.crud.server_squad import get_all_server_squads
from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id
from app.database.crud.tariff import get_tariff_by_id
@@ -1051,6 +1101,29 @@ async def _auto_purchase_daily_tariff(
_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
try:
if existing_subscription:
# Renewal/upgrade of existing subscription
await notify_user_subscription_renewed(
user_id=user.id,
new_expires_at=subscription.end_date.isoformat() if subscription.end_date else '',
amount_kopeks=daily_price,
)
else:
# New subscription activation
await notify_user_subscription_activated(
user_id=user.id,
expires_at=subscription.end_date.isoformat() if subscription.end_date else '',
tariff_name=tariff.name,
)
except Exception as ws_error:
logger.warning(
'⚠️ Автопокупка суточного тарифа: не удалось отправить WS уведомление для %s: %s',
_format_user_id(user),
ws_error,
)
return True
@@ -1061,6 +1134,11 @@ async def auto_purchase_saved_cart_after_topup(
bot: Bot | None = None,
) -> bool:
"""Attempts to automatically purchase a subscription from a saved cart."""
# Lazy imports to avoid circular dependency
from app.cabinet.routes.websocket import (
notify_user_subscription_activated,
notify_user_subscription_renewed,
)
if not settings.is_auto_purchase_after_topup_enabled():
return False
@@ -1243,6 +1321,29 @@ async def auto_purchase_saved_cart_after_topup(
_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
try:
if was_trial_conversion:
# Trial conversion = activation
await notify_user_subscription_activated(
user_id=user.id,
expires_at=subscription.end_date.isoformat() if subscription and subscription.end_date else '',
tariff_name='',
)
else:
# Regular purchase = renewal or new activation
await notify_user_subscription_renewed(
user_id=user.id,
new_expires_at=subscription.end_date.isoformat() if subscription and subscription.end_date else '',
amount_kopeks=pricing.final_total,
)
except Exception as ws_error:
logger.warning(
'⚠️ Автопокупка: не удалось отправить WS уведомление для %s: %s',
_format_user_id(user),
ws_error,
)
return True
@@ -1273,6 +1374,11 @@ async def auto_activate_subscription_after_topup(
"""
from datetime import datetime
# Lazy imports to avoid circular dependency
from app.cabinet.routes.websocket import (
notify_user_subscription_activated,
notify_user_subscription_renewed,
)
from app.database.crud.server_squad import get_available_server_squads, get_server_ids_by_uuids
from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id
from app.database.crud.transaction import create_transaction
@@ -1397,6 +1503,20 @@ async def auto_activate_subscription_after_topup(
best_price,
)
# Send WebSocket notification to cabinet frontend
try:
await notify_user_subscription_renewed(
user_id=user.id,
new_expires_at=result.subscription.end_date.isoformat() if result.subscription.end_date else '',
amount_kopeks=best_price,
)
except Exception as ws_error:
logger.warning(
'⚠️ Автоактивация: не удалось отправить WS уведомление о продлении для %s: %s',
_format_user_id(user),
ws_error,
)
# Уведомление пользователю (только для Telegram-пользователей)
if bot and user.telegram_id:
try:
@@ -1475,6 +1595,20 @@ async def auto_activate_subscription_after_topup(
best_price,
)
# Send WebSocket notification to cabinet frontend
try:
await notify_user_subscription_activated(
user_id=user.id,
expires_at=new_subscription.end_date.isoformat() if new_subscription.end_date else '',
tariff_name='',
)
except Exception as ws_error:
logger.warning(
'⚠️ Автоактивация: не удалось отправить WS уведомление об активации для %s: %s',
_format_user_id(user),
ws_error,
)
# Уведомление пользователю (только для Telegram-пользователей)
if bot and user.telegram_id:
try:
@@ -1542,9 +1676,11 @@ async def auto_activate_subscription_after_topup(
e,
exc_info=True,
)
try:
await db.rollback()
except Exception:
pass
return (False, False)
await db.rollback()
return False
__all__ = ['auto_activate_subscription_after_topup', 'auto_purchase_saved_cart_after_topup']
+9 -4
View File
@@ -52,6 +52,7 @@ class UserCartService:
"""
client = self._get_redis_client()
if client is None:
logger.warning(f'🛒 Redis недоступен, корзина пользователя {user_id} НЕ сохранена')
return False
try:
@@ -59,10 +60,11 @@ class UserCartService:
json_data = json.dumps(cart_data, ensure_ascii=False)
effective_ttl = ttl if ttl is not None else settings.CART_TTL_SECONDS
await client.setex(key, effective_ttl, json_data)
logger.debug(f'Корзина пользователя {user_id} сохранена в Redis')
cart_mode = cart_data.get('cart_mode', 'unknown')
logger.info(f'🛒 Корзина пользователя {user_id} сохранена в Redis (mode={cart_mode}, ttl={effective_ttl}s)')
return True
except Exception as e:
logger.error(f'Ошибка сохранения корзины пользователя {user_id}: {e}')
logger.error(f'🛒 Ошибка сохранения корзины пользователя {user_id}: {e}')
return False
async def get_user_cart(self, user_id: int) -> dict[str, Any] | None:
@@ -127,14 +129,17 @@ class UserCartService:
"""
client = self._get_redis_client()
if client is None:
logger.warning(f'🛒 Redis недоступен, проверка корзины пользователя {user_id} невозможна')
return False
try:
key = f'user_cart:{user_id}'
exists = await client.exists(key)
return bool(exists)
result = bool(exists)
logger.info(f'🛒 Проверка корзины пользователя {user_id}: {"найдена" if result else "не найдена"}')
return result
except Exception as e:
logger.error(f'Ошибка проверки наличия корзины пользователя {user_id}: {e}')
logger.error(f'🛒 Ошибка проверки наличия корзины пользователя {user_id}: {e}')
return False
+12
View File
@@ -49,6 +49,7 @@ from app.database.models import (
User,
UserMessage,
UserStatus,
WataPayment,
WelcomeText,
YooKassaPayment,
)
@@ -1055,6 +1056,17 @@ class UserService:
except Exception as e:
logger.error(f'❌ Ошибка удаления подписки: {e}')
try:
wata_payments_result = await db.execute(select(WataPayment).where(WataPayment.user_id == user_id))
wata_payments = wata_payments_result.scalars().all()
if wata_payments:
logger.info(f'🔄 Удаляем {len(wata_payments)} Wata платежей')
await db.execute(delete(WataPayment).where(WataPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error(f'❌ Ошибка удаления Wata платежей: {e}')
try:
await db.execute(delete(User).where(User.id == user_id))
await db.commit()