Files
remnawave-bedolaga-telegram…/app/cabinet/schemas/users.py
2026-01-17 06:28:42 +03:00

456 lines
13 KiB
Python

"""Schemas for Admin Users management in cabinet."""
from datetime import datetime
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
from enum import Enum
class UserStatusEnum(str, Enum):
"""User status enum."""
ACTIVE = "active"
BLOCKED = "blocked"
DELETED = "deleted"
class SubscriptionStatusEnum(str, Enum):
"""Subscription status enum."""
TRIAL = "trial"
ACTIVE = "active"
EXPIRED = "expired"
DISABLED = "disabled"
PENDING = "pending"
class SortByEnum(str, Enum):
"""Sort options for users list."""
CREATED_AT = "created_at"
BALANCE = "balance"
TRAFFIC = "traffic"
LAST_ACTIVITY = "last_activity"
TOTAL_SPENT = "total_spent"
PURCHASE_COUNT = "purchase_count"
# === User Subscription Info ===
class UserSubscriptionInfo(BaseModel):
"""User subscription information."""
id: int
status: str
is_trial: bool
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
traffic_limit_gb: int = 0
traffic_used_gb: float = 0.0
device_limit: int = 1
tariff_id: Optional[int] = None
tariff_name: Optional[str] = None
autopay_enabled: bool = False
is_active: bool = False
days_remaining: int = 0
class UserPromoGroupInfo(BaseModel):
"""User promo group info."""
id: int
name: str
is_default: bool = False
# === User List ===
class UserListItem(BaseModel):
"""User item in list."""
id: int
telegram_id: int
username: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
full_name: str
status: str
balance_kopeks: int
balance_rubles: float
created_at: datetime
last_activity: Optional[datetime] = None
# Subscription summary
has_subscription: bool = False
subscription_status: Optional[str] = None
subscription_is_trial: bool = False
subscription_end_date: Optional[datetime] = None
# Promo group
promo_group_id: Optional[int] = None
promo_group_name: Optional[str] = None
# Stats
total_spent_kopeks: int = 0
purchase_count: int = 0
# Restrictions
has_restrictions: bool = False
restriction_topup: bool = False
restriction_subscription: bool = False
class UsersListResponse(BaseModel):
"""Paginated list of users."""
users: List[UserListItem]
total: int
offset: int = 0
limit: int = 50
# === User Detail ===
class UserTransactionItem(BaseModel):
"""User transaction."""
id: int
type: str
amount_kopeks: int
amount_rubles: float
description: Optional[str] = None
payment_method: Optional[str] = None
is_completed: bool = True
created_at: datetime
class UserReferralInfo(BaseModel):
"""User referral info."""
referral_code: str
referrals_count: int = 0
total_earnings_kopeks: int = 0
commission_percent: Optional[int] = None
referred_by_id: Optional[int] = None
referred_by_username: Optional[str] = None
class UserDetailResponse(BaseModel):
"""Detailed user information."""
id: int
telegram_id: int
username: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
full_name: str
status: str
language: str
balance_kopeks: int
balance_rubles: float
# Email (cabinet)
email: Optional[str] = None
email_verified: bool = False
# Dates
created_at: datetime
updated_at: Optional[datetime] = None
last_activity: Optional[datetime] = None
cabinet_last_login: Optional[datetime] = None
# Subscription
subscription: Optional[UserSubscriptionInfo] = None
# Promo group
promo_group: Optional[UserPromoGroupInfo] = None
# Referral
referral: UserReferralInfo
# Stats
total_spent_kopeks: int = 0
purchase_count: int = 0
used_promocodes: int = 0
has_had_paid_subscription: bool = False
lifetime_used_traffic_bytes: int = 0
# Restrictions
restriction_topup: bool = False
restriction_subscription: bool = False
restriction_reason: Optional[str] = None
# Promo offer
promo_offer_discount_percent: int = 0
promo_offer_discount_source: Optional[str] = None
promo_offer_discount_expires_at: Optional[datetime] = None
# Recent transactions
recent_transactions: List[UserTransactionItem] = []
# === User Actions ===
class UpdateBalanceRequest(BaseModel):
"""Request to update user balance."""
amount_kopeks: int = Field(..., description="Amount in kopeks (positive to add, negative to subtract)")
description: str = Field(default="Admin balance adjustment", max_length=500)
create_transaction: bool = Field(default=True, description="Create transaction record")
class UpdateBalanceResponse(BaseModel):
"""Response after balance update."""
success: bool
old_balance_kopeks: int
new_balance_kopeks: int
message: str
class UpdateSubscriptionRequest(BaseModel):
"""Request to update user subscription."""
action: str = Field(..., description="Action: extend, set_end_date, change_tariff, set_traffic, toggle_autopay, cancel")
# For extend action
days: Optional[int] = Field(None, ge=1, le=3650, description="Days to extend")
# For set_end_date action
end_date: Optional[datetime] = Field(None, description="New end date")
# For change_tariff action
tariff_id: Optional[int] = Field(None, description="New tariff ID")
# For set_traffic action
traffic_limit_gb: Optional[int] = Field(None, ge=0, description="New traffic limit in GB")
traffic_used_gb: Optional[float] = Field(None, ge=0, description="Set traffic used in GB")
# For toggle_autopay
autopay_enabled: Optional[bool] = Field(None, description="Enable/disable autopay")
# For create new subscription
is_trial: Optional[bool] = Field(None, description="Is trial subscription")
device_limit: Optional[int] = Field(None, ge=1, description="Device limit")
class UpdateSubscriptionResponse(BaseModel):
"""Response after subscription update."""
success: bool
message: str
subscription: Optional[UserSubscriptionInfo] = None
class UpdateUserStatusRequest(BaseModel):
"""Request to update user status."""
status: UserStatusEnum
reason: Optional[str] = Field(None, max_length=500, description="Reason for status change")
class UpdateUserStatusResponse(BaseModel):
"""Response after status update."""
success: bool
old_status: str
new_status: str
message: str
class UpdateRestrictionsRequest(BaseModel):
"""Request to update user restrictions."""
restriction_topup: Optional[bool] = Field(None, description="Block balance top-up")
restriction_subscription: Optional[bool] = Field(None, description="Block subscription purchase/renewal")
restriction_reason: Optional[str] = Field(None, max_length=500, description="Reason for restrictions")
class UpdateRestrictionsResponse(BaseModel):
"""Response after restrictions update."""
success: bool
restriction_topup: bool
restriction_subscription: bool
restriction_reason: Optional[str] = None
message: str
class UpdatePromoGroupRequest(BaseModel):
"""Request to update user promo group."""
promo_group_id: Optional[int] = Field(None, description="New promo group ID (null to remove)")
class UpdatePromoGroupResponse(BaseModel):
"""Response after promo group update."""
success: bool
old_promo_group_id: Optional[int] = None
new_promo_group_id: Optional[int] = None
promo_group_name: Optional[str] = None
message: str
class DeleteUserRequest(BaseModel):
"""Request to delete user."""
soft_delete: bool = Field(default=True, description="Soft delete (mark as deleted) or hard delete")
reason: Optional[str] = Field(None, max_length=500, description="Reason for deletion")
class DeleteUserResponse(BaseModel):
"""Response after user deletion."""
success: bool
message: str
# === Statistics ===
class UsersStatsResponse(BaseModel):
"""Users statistics."""
total_users: int = 0
active_users: int = 0
blocked_users: int = 0
deleted_users: int = 0
new_today: int = 0
new_week: int = 0
new_month: int = 0
# Subscription stats
users_with_subscription: int = 0
users_with_active_subscription: int = 0
users_with_trial: int = 0
users_with_expired_subscription: int = 0
# Financial stats
total_balance_kopeks: int = 0
total_balance_rubles: float = 0.0
avg_balance_kopeks: int = 0
# Activity stats
active_today: int = 0
active_week: int = 0
active_month: int = 0
# === Search ===
class UserSearchRequest(BaseModel):
"""Request for user search."""
query: str = Field(..., min_length=1, max_length=255)
search_by: List[str] = Field(
default=["telegram_id", "username", "first_name", "last_name", "email"],
description="Fields to search in"
)
limit: int = Field(default=20, ge=1, le=100)
# === Tariffs for User ===
class PeriodPriceInfo(BaseModel):
"""Period price info."""
days: int
price_kopeks: int
price_rubles: float
class UserAvailableTariffItem(BaseModel):
"""Tariff available for user."""
id: int
name: str
description: Optional[str] = None
is_active: bool = True
is_trial_available: bool = False
traffic_limit_gb: int = 0
device_limit: int = 1
tier_level: int = 1
display_order: int = 0
# Pricing
period_prices: List[PeriodPriceInfo] = []
is_daily: bool = False
daily_price_kopeks: int = 0
# Custom options
custom_days_enabled: bool = False
price_per_day_kopeks: int = 0
min_days: int = 1
max_days: int = 365
# Access info
is_available: bool = True # Available for this user's promo group
requires_promo_group: bool = False # Requires specific promo group
class UserAvailableTariffsResponse(BaseModel):
"""List of tariffs available for user."""
user_id: int
promo_group_id: Optional[int] = None
promo_group_name: Optional[str] = None
tariffs: List[UserAvailableTariffItem] = []
total: int = 0
# Current subscription tariff
current_tariff_id: Optional[int] = None
current_tariff_name: Optional[str] = None
# === Panel Sync ===
class PanelUserInfo(BaseModel):
"""User info from panel."""
uuid: Optional[str] = None
short_uuid: Optional[str] = None
username: Optional[str] = None
status: Optional[str] = None
expire_at: Optional[datetime] = None
traffic_limit_gb: float = 0
traffic_used_gb: float = 0
device_limit: int = 1
subscription_url: Optional[str] = None
active_squads: List[str] = []
class SyncFromPanelRequest(BaseModel):
"""Request to sync user from panel."""
update_subscription: bool = Field(default=True, description="Update subscription data")
update_traffic: bool = Field(default=True, description="Update traffic usage")
create_if_missing: bool = Field(default=False, description="Create subscription if user exists in panel but not in bot")
class SyncFromPanelResponse(BaseModel):
"""Response after syncing from panel."""
success: bool
message: str
panel_user: Optional[PanelUserInfo] = None
changes: Dict[str, Any] = {}
errors: List[str] = []
class SyncToPanelRequest(BaseModel):
"""Request to sync user to panel."""
create_if_missing: bool = Field(default=True, description="Create user in panel if not exists")
update_status: bool = Field(default=True, description="Update user status in panel")
update_traffic_limit: bool = Field(default=True, description="Update traffic limit in panel")
update_expire_date: bool = Field(default=True, description="Update expire date in panel")
update_squads: bool = Field(default=True, description="Update connected squads in panel")
class SyncToPanelResponse(BaseModel):
"""Response after syncing to panel."""
success: bool
message: str
action: str = "" # created, updated, no_changes
panel_uuid: Optional[str] = None
changes: Dict[str, Any] = {}
errors: List[str] = []
class PanelSyncStatusResponse(BaseModel):
"""Panel sync status for user."""
user_id: int
telegram_id: int
remnawave_uuid: Optional[str] = None
last_sync: Optional[datetime] = None
# Bot data
bot_subscription_status: Optional[str] = None
bot_subscription_end_date: Optional[datetime] = None
bot_traffic_limit_gb: int = 0
bot_traffic_used_gb: float = 0
bot_device_limit: int = 0
bot_squads: List[str] = []
# Panel data (if available)
panel_found: bool = False
panel_status: Optional[str] = None
panel_expire_at: Optional[datetime] = None
panel_traffic_limit_gb: float = 0
panel_traffic_used_gb: float = 0
panel_device_limit: int = 0
panel_squads: List[str] = []
# Differences
has_differences: bool = False
differences: List[str] = []