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/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