diff --git a/app/cabinet/routes/balance.py b/app/cabinet/routes/balance.py
index 1a048d82..1a72121a 100644
--- a/app/cabinet/routes/balance.py
+++ b/app/cabinet/routes/balance.py
@@ -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:
diff --git a/app/cabinet/routes/subscription.py b/app/cabinet/routes/subscription.py
index 11b9a769..2ccbf4a6 100644
--- a/app/cabinet/routes/subscription.py
+++ b/app/cabinet/routes/subscription.py
@@ -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,
diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py
index 9a6af9e1..4781f595 100644
--- a/app/services/monitoring_service.py
+++ b/app/services/monitoring_service.py
@@ -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 = """
π Π’Π΅ΡΡΠΎΠ²Π°Ρ ΠΏΠΎΠ΄ΠΏΠΈΡΠΊΠ° ΡΠΊΠΎΡΠΎ Π·Π°ΠΊΠΎΠ½ΡΠΈΡΡΡ!
ΠΠ°ΡΠ° ΡΠ΅ΡΡΠΎΠ²Π°Ρ ΠΏΠΎΠ΄ΠΏΠΈΡΠΊΠ° ΠΈΡΡΠ΅ΠΊΠ°Π΅Ρ ΡΠ΅ΡΠ΅Π· 2 ΡΠ°ΡΠ°.
@@ -1251,12 +1201,6 @@ class MonitoringService:
π ΠΠ΅ Ρ
ΠΎΡΠΈΡΠ΅ ΠΎΡΡΠ°ΡΡΡΡ Π±Π΅Π· VPN?
ΠΠ΅ΡΠ΅Ρ
ΠΎΠ΄ΠΈΡΠ΅ Π½Π° ΠΏΠΎΠ»Π½ΡΡ ΠΏΠΎΠ΄ΠΏΠΈΡΠΊΡ!
-π₯ Π‘ΠΏΠ΅ΡΠΈΠ°Π»ΡΠ½ΠΎΠ΅ ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅:
-β’ 30 Π΄Π½Π΅ΠΉ Π²ΡΠ΅Π³ΠΎ Π·Π° {settings.format_price(min_total_price)}
-β’ ΠΠ΅Π·Π»ΠΈΠΌΠΈΡΠ½ΡΠΉ ΡΡΠ°ΡΠΈΠΊ
-β’ ΠΡΠ΅ ΡΠ΅ΡΠ²Π΅ΡΡ Π΄ΠΎΡΡΡΠΏΠ½Ρ
-β’ Π‘ΠΊΠΎΡΠΎΡΡΡ Π΄ΠΎ 1ΠΠΠΈΡ/ΡΠ΅ΠΊ
-
β‘οΈ Π£ΡΠΏΠ΅ΠΉΡΠ΅ ΠΎΡΠΎΡΠΌΠΈΡΡ Π΄ΠΎ ΠΎΠΊΠΎΠ½ΡΠ°Π½ΠΈΡ ΡΠ΅ΡΡΠΎΠ²ΠΎΠ³ΠΎ ΠΏΠ΅ΡΠΈΠΎΠ΄Π°!
"""
diff --git a/app/services/payment/common.py b/app/services/payment/common.py
index ce819f62..a4a2d3d7 100644
--- a/app/services/payment/common.py
+++ b/app/services/payment/common.py
@@ -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
diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py
index 9149d0c6..310e75f2 100644
--- a/app/services/payment/heleket.py
+++ b/app/services/payment/heleket.py
@@ -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(
diff --git a/app/services/payment_method_config_service.py b/app/services/payment_method_config_service.py
index 34d183f5..f06f637e 100644
--- a/app/services/payment_method_config_service.py
+++ b/app/services/payment_method_config_service.py
@@ -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
diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py
index aa5e1ed2..64b5994c 100644
--- a/app/services/subscription_auto_purchase_service.py
+++ b/app/services/subscription_auto_purchase_service.py
@@ -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']
diff --git a/app/services/user_cart_service.py b/app/services/user_cart_service.py
index f7901be4..f9522d28 100644
--- a/app/services/user_cart_service.py
+++ b/app/services/user_cart_service.py
@@ -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
diff --git a/app/services/user_service.py b/app/services/user_service.py
index 849799bd..c811400f 100644
--- a/app/services/user_service.py
+++ b/app/services/user_service.py
@@ -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()