Add files via upload

This commit is contained in:
Egor
2026-01-26 22:37:32 +03:00
committed by GitHub
parent 441bc44a24
commit 7019799f4c
3 changed files with 357 additions and 157 deletions

View File

@@ -7,6 +7,7 @@ from .admin_ban_system import router as admin_ban_system_router
from .admin_email_templates import router as admin_email_templates_router
from .admin_broadcasts import router as admin_broadcasts_router
from .admin_campaigns import router as admin_campaigns_router
from .admin_payment_methods import router as admin_payment_methods_router
from .admin_payments import router as admin_payments_router
from .admin_promo_offers import router as admin_promo_offers_router
from .admin_promocodes import promo_groups_router as admin_promo_groups_router, router as admin_promocodes_router
@@ -77,6 +78,7 @@ router.include_router(admin_promocodes_router)
router.include_router(admin_promo_groups_router)
router.include_router(admin_campaigns_router)
router.include_router(admin_users_router)
router.include_router(admin_payment_methods_router)
router.include_router(admin_payments_router)
router.include_router(admin_promo_offers_router)
router.include_router(admin_remnawave_router)

View File

@@ -0,0 +1,228 @@
"""Admin routes for payment method configuration in cabinet."""
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User
from app.services.payment_method_config_service import (
_get_method_defaults,
get_all_configs,
get_all_promo_groups,
get_config_by_method_id,
update_config,
update_sort_order,
)
from ..dependencies import get_cabinet_db, get_current_admin_user
logger = logging.getLogger(__name__)
router = APIRouter(prefix='/admin/payment-methods', tags=['Cabinet Admin Payment Methods'])
# ============ Schemas ============
class SubOptionInfo(BaseModel):
id: str
name: str
class PaymentMethodConfigResponse(BaseModel):
method_id: str
sort_order: int
is_enabled: bool
display_name: str | None = None
default_display_name: str
sub_options: dict | None = None
available_sub_options: list[SubOptionInfo] | None = None
min_amount_kopeks: int | None = None
max_amount_kopeks: int | None = None
default_min_amount_kopeks: int
default_max_amount_kopeks: int
user_type_filter: str
first_topup_filter: str
promo_group_filter_mode: str
allowed_promo_group_ids: list[int] = Field(default_factory=list)
is_provider_configured: bool
created_at: datetime | None = None
updated_at: datetime | None = None
class Config:
from_attributes = True
class PaymentMethodConfigUpdateRequest(BaseModel):
is_enabled: bool | None = None
display_name: str | None = Field(default=None, description='Null to reset to default')
sub_options: dict | None = None
min_amount_kopeks: int | None = Field(default=None, ge=0)
max_amount_kopeks: int | None = Field(default=None, ge=0)
user_type_filter: str | None = Field(default=None, pattern='^(all|telegram|email)$')
first_topup_filter: str | None = Field(default=None, pattern='^(any|yes|no)$')
promo_group_filter_mode: str | None = Field(default=None, pattern='^(all|selected)$')
allowed_promo_group_ids: list[int] | None = None
# Allow explicitly resetting display_name to null
reset_display_name: bool = False
reset_min_amount: bool = False
reset_max_amount: bool = False
class SortOrderRequest(BaseModel):
method_ids: list[str]
class PromoGroupSimple(BaseModel):
id: int
name: str
class Config:
from_attributes = True
# ============ Helpers ============
def _enrich_config(config, defaults: dict) -> PaymentMethodConfigResponse:
"""Enrich a PaymentMethodConfig with env-var defaults."""
method_def = defaults.get(config.method_id, {})
available_sub_options = None
raw_options = method_def.get('available_sub_options')
if raw_options:
available_sub_options = [SubOptionInfo(**opt) for opt in raw_options]
return PaymentMethodConfigResponse(
method_id=config.method_id,
sort_order=config.sort_order,
is_enabled=config.is_enabled,
display_name=config.display_name,
default_display_name=method_def.get('default_display_name', config.method_id),
sub_options=config.sub_options,
available_sub_options=available_sub_options,
min_amount_kopeks=config.min_amount_kopeks,
max_amount_kopeks=config.max_amount_kopeks,
default_min_amount_kopeks=method_def.get('default_min', 1000),
default_max_amount_kopeks=method_def.get('default_max', 10000000),
user_type_filter=config.user_type_filter,
first_topup_filter=config.first_topup_filter,
promo_group_filter_mode=config.promo_group_filter_mode,
allowed_promo_group_ids=[pg.id for pg in config.allowed_promo_groups],
is_provider_configured=method_def.get('is_configured', False),
created_at=config.created_at,
updated_at=config.updated_at,
)
# ============ Routes ============
@router.get('', response_model=list[PaymentMethodConfigResponse])
async def list_payment_methods(
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""List all payment method configurations."""
configs = await get_all_configs(db)
defaults = _get_method_defaults()
return [_enrich_config(c, defaults) for c in configs]
@router.get('/promo-groups', response_model=list[PromoGroupSimple])
async def list_promo_groups(
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""List all promo groups for filter selector."""
groups = await get_all_promo_groups(db)
return [PromoGroupSimple(id=g.id, name=g.name) for g in groups]
@router.get('/{method_id}', response_model=PaymentMethodConfigResponse)
async def get_payment_method(
method_id: str,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get a single payment method configuration."""
config = await get_config_by_method_id(db, method_id)
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f'Payment method not found: {method_id}',
)
defaults = _get_method_defaults()
return _enrich_config(config, defaults)
@router.put('/order')
async def update_payment_methods_order(
request: SortOrderRequest,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Batch update sort order for payment methods."""
await update_sort_order(db, request.method_ids)
logger.info(f'Admin {admin.id} updated payment methods order: {request.method_ids}')
return {'success': True}
@router.put('/{method_id}', response_model=PaymentMethodConfigResponse)
async def update_payment_method(
method_id: str,
request: PaymentMethodConfigUpdateRequest,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Update a payment method configuration."""
# Build update data dict
data = {}
if request.is_enabled is not None:
data['is_enabled'] = request.is_enabled
if request.reset_display_name:
data['display_name'] = None
elif request.display_name is not None:
data['display_name'] = request.display_name.strip() or None
if request.sub_options is not None:
data['sub_options'] = request.sub_options
if request.reset_min_amount:
data['min_amount_kopeks'] = None
elif request.min_amount_kopeks is not None:
data['min_amount_kopeks'] = request.min_amount_kopeks
if request.reset_max_amount:
data['max_amount_kopeks'] = None
elif request.max_amount_kopeks is not None:
data['max_amount_kopeks'] = request.max_amount_kopeks
if request.user_type_filter is not None:
data['user_type_filter'] = request.user_type_filter
if request.first_topup_filter is not None:
data['first_topup_filter'] = request.first_topup_filter
if request.promo_group_filter_mode is not None:
data['promo_group_filter_mode'] = request.promo_group_filter_mode
promo_group_ids = request.allowed_promo_group_ids
config = await update_config(db, method_id, data, promo_group_ids)
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f'Payment method not found: {method_id}',
)
logger.info(f'Admin {admin.id} updated payment method config: {method_id}')
defaults = _get_method_defaults()
return _enrich_config(config, defaults)

View File

@@ -127,179 +127,149 @@ async def get_transactions(
)
@router.get('/payment-methods', response_model=list[PaymentMethodResponse])
async def get_payment_methods():
"""Get available payment methods."""
methods = []
async def _get_available_payment_methods(
db: AsyncSession,
user: User,
) -> list[PaymentMethodResponse]:
"""Get available payment methods filtered by DB config and user context.
# 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)'},
],
)
)
Combines env-var availability with DB-based admin config (ordering, display conditions).
"""
from app.services.payment_method_config_service import (
_get_method_defaults,
get_all_configs,
)
# 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,
)
)
configs = await get_all_configs(db)
defaults = _get_method_defaults()
# 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,
)
)
# Provider availability checks from env vars
provider_enabled = {
'telegram_stars': settings.TELEGRAM_STARS_ENABLED,
'tribute': settings.TRIBUTE_ENABLED and bool(getattr(settings, 'TRIBUTE_DONATE_LINK', '')),
'cryptobot': settings.is_cryptobot_enabled(),
'heleket': settings.is_heleket_enabled(),
'yookassa': settings.is_yookassa_enabled(),
'mulenpay': settings.is_mulenpay_enabled(),
'pal24': settings.is_pal24_enabled(),
'platega': settings.is_platega_enabled(),
'wata': settings.is_wata_enabled(),
'freekassa': settings.is_freekassa_enabled(),
'cloudpayments': settings.is_cloudpayments_enabled(),
}
# 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(
{
# Default options builder (for methods with sub-options)
def _build_options(method_id: str, config_sub_options: dict | None) -> list[dict] | None:
if method_id == 'yookassa':
all_opts = [
{'id': 'card', 'name': '💳 Карта', 'description': 'Банковская карта'},
{'id': 'sbp', 'name': '🏦 СБП', 'description': 'Система быстрых платежей (QR)'},
]
elif method_id == 'pal24':
all_opts = [
{'id': 'sbp', 'name': '🏦 СБП', 'description': 'Система быстрых платежей'},
{'id': 'card', 'name': '💳 Карта', 'description': 'Банковская карта'},
]
elif method_id == 'platega':
platega_methods = settings.get_platega_active_methods()
definitions = settings.get_platega_method_definitions()
all_opts = []
for method_code in platega_methods:
info = definitions.get(method_code, {})
all_opts.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 '',
}
)
})
elif method_id == 'freekassa':
all_opts = [
{'id': 'sbp', 'name': '🏦 NSPK СБП', 'description': 'Система быстрых платежей'},
{'id': 'card', 'name': '💳 Карта', 'description': 'Банковская карта'},
]
elif method_id == 'cloudpayments':
all_opts = [
{'id': 'card', 'name': '💳 Карта', 'description': 'Банковская карта'},
{'id': 'sbp', 'name': '🏦 СБП', 'description': 'Система быстрых платежей'},
]
else:
return None
if not all_opts:
return None
# Filter by sub_options config from DB
if config_sub_options:
all_opts = [o for o in all_opts if config_sub_options.get(o['id'], True)]
return all_opts if all_opts else None
# User promo group IDs for filtering
user_promo_group_ids: set[int] = set()
if hasattr(user, 'user_promo_groups') and user.user_promo_groups:
for upg in user.user_promo_groups:
user_promo_group_ids.add(upg.promo_group_id)
if hasattr(user, 'promo_group_id') and user.promo_group_id:
user_promo_group_ids.add(user.promo_group_id)
methods = []
for config in configs:
mid = config.method_id
# 1. Check env-var provider availability AND DB admin toggle
if not provider_enabled.get(mid, False):
continue
if not config.is_enabled:
continue
# 2. Check user type filter
if config.user_type_filter == 'telegram' and user.auth_type != 'telegram':
continue
if config.user_type_filter == 'email' and user.auth_type != 'email':
continue
# 3. Check first topup filter
if config.first_topup_filter == 'yes' and not user.has_made_first_topup:
continue
if config.first_topup_filter == 'no' and user.has_made_first_topup:
continue
# 4. Check promo group filter
if config.promo_group_filter_mode == 'selected' and config.allowed_promo_groups:
allowed_ids = {pg.id for pg in config.allowed_promo_groups}
if not user_promo_group_ids.intersection(allowed_ids):
continue
# Build the response
method_def = defaults.get(mid, {})
display_name = config.display_name or method_def.get('default_display_name', mid)
min_amount = config.min_amount_kopeks or method_def.get('default_min', 1000)
max_amount = config.max_amount_kopeks or method_def.get('default_max', 10000000)
options = _build_options(mid, config.sub_options)
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,
)
)
# 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=mid,
name=display_name,
description=None,
min_amount_kopeks=min_amount,
max_amount_kopeks=max_amount,
is_available=True,
options=options,
)
)
return methods
@router.get('/payment-methods', response_model=list[PaymentMethodResponse])
async def get_payment_methods(
user: User = Depends(get_current_cabinet_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get available payment methods."""
return await _get_available_payment_methods(db, user)
@router.post('/stars-invoice', response_model=StarsInvoiceResponse)
async def create_stars_invoice(
request: StarsInvoiceRequest,
@@ -401,7 +371,7 @@ async def create_topup(
):
"""Create payment for balance top-up."""
# Validate payment method
methods = await get_payment_methods()
methods = await _get_available_payment_methods(db, user)
method = next((m for m in methods if m.id == request.payment_method), None)
if not method or not method.is_available: