diff --git a/.env.example b/.env.example index f1d6c807..e0ab9878 100644 --- a/.env.example +++ b/.env.example @@ -371,7 +371,8 @@ REFERRAL_MINIMUM_TOPUP_KOPEKS=10000 REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=10000 REFERRAL_INVITER_BONUS_KOPEKS=10000 REFERRAL_COMMISSION_PERCENT=25 - +# Показывать раздел партнёрки в кабинете +REFERRAL_PARTNER_SECTION_VISIBLE=true # Уведомления REFERRAL_NOTIFICATIONS_ENABLED=true @@ -384,6 +385,8 @@ REFERRAL_WITHDRAWAL_ENABLED=false REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS=50000 # Интервал между запросами на вывод (дни) REFERRAL_WITHDRAWAL_COOLDOWN_DAYS=30 +# Текст-подсказка для поля реквизитов при выводе (пустая строка = стандартный текст) +REFERRAL_WITHDRAWAL_REQUISITES_TEXT= # Выводить только реферальный баланс (true) или весь баланс (false) REFERRAL_WITHDRAWAL_ONLY_REFERRAL_BALANCE=true # ID топика для уведомлений о заявках на вывод (0 = основной чат) diff --git a/app/cabinet/routes/admin_partners.py b/app/cabinet/routes/admin_partners.py index 546ee806..dc35fa8b 100644 --- a/app/cabinet/routes/admin_partners.py +++ b/app/cabinet/routes/admin_partners.py @@ -4,9 +4,11 @@ from typing import Literal import structlog from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import BaseModel, Field from sqlalchemy import desc, func, select from sqlalchemy.ext.asyncio import AsyncSession +from app.config import settings from app.database.models import ( AdvertisingCampaign, PartnerApplication, @@ -36,6 +38,116 @@ logger = structlog.get_logger(__name__) router = APIRouter(prefix='/admin/partners', tags=['Cabinet Admin Partners']) +# ==================== Settings ==================== + + +class PartnerSettingsResponse(BaseModel): + withdrawal_enabled: bool + withdrawal_min_amount_kopeks: int + withdrawal_cooldown_days: int + withdrawal_requisites_text: str + partner_section_visible: bool + referral_program_enabled: bool + + +class PartnerSettingsUpdateRequest(BaseModel): + withdrawal_enabled: bool | None = None + withdrawal_min_amount_kopeks: int | None = Field(None, ge=0, le=100_000_000) + withdrawal_cooldown_days: int | None = Field(None, ge=0, le=365) + withdrawal_requisites_text: str | None = Field(None, max_length=2000) + partner_section_visible: bool | None = None + referral_program_enabled: bool | None = None + + +def _build_partner_settings_response() -> PartnerSettingsResponse: + return PartnerSettingsResponse( + withdrawal_enabled=settings.REFERRAL_WITHDRAWAL_ENABLED, + withdrawal_min_amount_kopeks=settings.REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS, + withdrawal_cooldown_days=settings.REFERRAL_WITHDRAWAL_COOLDOWN_DAYS, + withdrawal_requisites_text=settings.REFERRAL_WITHDRAWAL_REQUISITES_TEXT, + partner_section_visible=settings.REFERRAL_PARTNER_SECTION_VISIBLE, + referral_program_enabled=settings.REFERRAL_PROGRAM_ENABLED, + ) + + +@router.get('/settings', response_model=PartnerSettingsResponse) +async def get_partner_settings( + admin: User = Depends(get_current_admin_user), +): + """Get partner system settings.""" + return _build_partner_settings_response() + + +@router.patch('/settings', response_model=PartnerSettingsResponse) +async def update_partner_settings( + request: PartnerSettingsUpdateRequest, + admin: User = Depends(get_current_admin_user), +): + """Update partner system settings.""" + from pathlib import Path + + # Update in-memory settings + if request.withdrawal_enabled is not None: + settings.REFERRAL_WITHDRAWAL_ENABLED = request.withdrawal_enabled + if request.withdrawal_min_amount_kopeks is not None: + settings.REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS = request.withdrawal_min_amount_kopeks + if request.withdrawal_cooldown_days is not None: + settings.REFERRAL_WITHDRAWAL_COOLDOWN_DAYS = request.withdrawal_cooldown_days + if request.withdrawal_requisites_text is not None: + settings.REFERRAL_WITHDRAWAL_REQUISITES_TEXT = request.withdrawal_requisites_text + if request.partner_section_visible is not None: + settings.REFERRAL_PARTNER_SECTION_VISIBLE = request.partner_section_visible + if request.referral_program_enabled is not None: + settings.REFERRAL_PROGRAM_ENABLED = request.referral_program_enabled + + # Persist to .env file + try: + env_file = Path('.env') + if env_file.exists(): + lines = env_file.read_text().splitlines() + updates: dict[str, str] = {} + + if request.withdrawal_enabled is not None: + updates['REFERRAL_WITHDRAWAL_ENABLED'] = str(request.withdrawal_enabled).lower() + if request.withdrawal_min_amount_kopeks is not None: + updates['REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS'] = str(request.withdrawal_min_amount_kopeks) + if request.withdrawal_cooldown_days is not None: + updates['REFERRAL_WITHDRAWAL_COOLDOWN_DAYS'] = str(request.withdrawal_cooldown_days) + if request.withdrawal_requisites_text is not None: + # Sanitize: replace newlines to prevent .env injection + sanitized = request.withdrawal_requisites_text.replace('\r\n', ' ').replace('\n', ' ').replace('\r', ' ') + updates['REFERRAL_WITHDRAWAL_REQUISITES_TEXT'] = sanitized + if request.partner_section_visible is not None: + updates['REFERRAL_PARTNER_SECTION_VISIBLE'] = str(request.partner_section_visible).lower() + if request.referral_program_enabled is not None: + updates['REFERRAL_PROGRAM_ENABLED'] = str(request.referral_program_enabled).lower() + + new_lines = [] + updated_keys: set[str] = set() + + for line in lines: + updated = False + for key, value in updates.items(): + if line.startswith(f'{key}='): + new_lines.append(f'{key}={value}') + updated_keys.add(key) + updated = True + break + if not updated: + new_lines.append(line) + + for key, value in updates.items(): + if key not in updated_keys: + new_lines.append(f'{key}={value}') + + env_file.write_text('\n'.join(new_lines) + '\n') + logger.info('Updated partner settings in .env file', admin_id=admin.id) + except Exception as e: + logger.warning('Failed to update .env file', error=e) + + return _build_partner_settings_response() + + # ==================== Applications (static paths first) ==================== diff --git a/app/cabinet/routes/referral.py b/app/cabinet/routes/referral.py index 9c595dc8..e06d7e60 100644 --- a/app/cabinet/routes/referral.py +++ b/app/cabinet/routes/referral.py @@ -199,4 +199,5 @@ async def get_referral_terms(): first_topup_bonus_rubles=settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS / 100, inviter_bonus_kopeks=settings.REFERRAL_INVITER_BONUS_KOPEKS, inviter_bonus_rubles=settings.REFERRAL_INVITER_BONUS_KOPEKS / 100, + partner_section_visible=settings.REFERRAL_PARTNER_SECTION_VISIBLE, ) diff --git a/app/cabinet/routes/withdrawal.py b/app/cabinet/routes/withdrawal.py index 4d8c5a21..132298f3 100644 --- a/app/cabinet/routes/withdrawal.py +++ b/app/cabinet/routes/withdrawal.py @@ -44,6 +44,7 @@ async def get_withdrawal_balance( is_withdrawal_enabled=settings.is_referral_withdrawal_enabled(), can_request=can_request, cannot_request_reason=reason if not can_request else None, + requisites_text=settings.REFERRAL_WITHDRAWAL_REQUISITES_TEXT, ) diff --git a/app/cabinet/schemas/referral.py b/app/cabinet/schemas/referral.py index db0c61f8..bacf2e61 100644 --- a/app/cabinet/schemas/referral.py +++ b/app/cabinet/schemas/referral.py @@ -76,3 +76,4 @@ class ReferralTermsResponse(BaseModel): first_topup_bonus_rubles: float inviter_bonus_kopeks: int inviter_bonus_rubles: float + partner_section_visible: bool = True diff --git a/app/cabinet/schemas/withdrawals.py b/app/cabinet/schemas/withdrawals.py index 8500d721..6ceb05c3 100644 --- a/app/cabinet/schemas/withdrawals.py +++ b/app/cabinet/schemas/withdrawals.py @@ -22,6 +22,7 @@ class WithdrawalBalanceResponse(BaseModel): is_withdrawal_enabled: bool can_request: bool cannot_request_reason: str | None = None + requisites_text: str = '' class WithdrawalCreateRequest(BaseModel): diff --git a/app/config.py b/app/config.py index 826e005d..021399f9 100644 --- a/app/config.py +++ b/app/config.py @@ -230,7 +230,9 @@ class Settings(BaseSettings): REFERRAL_WITHDRAWAL_MIN_AMOUNT_KOPEKS: int = 100000 # Мин. сумма вывода (1000₽) REFERRAL_WITHDRAWAL_COOLDOWN_DAYS: int = 30 # Частота запросов на вывод REFERRAL_WITHDRAWAL_ONLY_REFERRAL_BALANCE: bool = True # Только реф. баланс (False = реф + свой) + REFERRAL_WITHDRAWAL_REQUISITES_TEXT: str = '' # Текст-подсказка для реквизитов при выводе REFERRAL_WITHDRAWAL_NOTIFICATIONS_TOPIC_ID: int | None = None # Топик для уведомлений + REFERRAL_PARTNER_SECTION_VISIBLE: bool = True # Показывать раздел партнёрки в кабинете # Настройки анализа на подозрительность REFERRAL_WITHDRAWAL_SUSPICIOUS_MIN_DEPOSIT_KOPEKS: int = 50000 # Мин. сумма от 1 реферала (500₽)