1f0fef114b
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding via structlog contextvars (aiogram) and http_method/http_path (FastAPI) - Use bound_contextvars() context manager instead of clear_contextvars() to safely restore previous state instead of wiping all context - Register ContextVarsMiddleware as outermost middleware (before GlobalError) so all error logs include user context - Replace structlog.get_logger() with structlog.get_logger(__name__) across 270 calls in 265 files for meaningful logger names - Switch wrapper_class from BoundLogger to make_filtering_bound_logger() for pre-processor level filtering (performance optimization) - Migrate 1411 %-style positional arg logger calls to structlog kwargs style across 161 files via AST script - Migrate log_rotation_service.py from stdlib logging to structlog - Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking to Telegram notifications and general log files - Fix LoggingMiddleware: add from_user null-safety for channel posts, switch time.time() to time.monotonic() for duration measurement - Remove duplicate logger assignments in purchase.py, config.py, inline.py, and admin/payments.py
229 lines
7.5 KiB
Python
229 lines
7.5 KiB
Python
"""Admin routes for payment method configuration in cabinet."""
|
|
|
|
from datetime import datetime
|
|
|
|
import structlog
|
|
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 = structlog.get_logger(__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('Admin updated payment methods order', admin_id=admin.id, method_ids=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('Admin updated payment method config', admin_id=admin.id, method_id=method_id)
|
|
|
|
defaults = _get_method_defaults()
|
|
return _enrich_config(config, defaults)
|