ded5c899f7
- Use PartnerStatus.APPROVED.value instead of hardcoded 'approved' - Extract shared deep_link/web_link helpers to cabinet/utils/links.py - Add _safe_div() helper for None-safe division - Add try/except error handling on campaign endpoints - Use model_fields_set for PATCH-style field detection - Replace deprecated class Config with ConfigDict(from_attributes=True) - Remove unnecessary selectinload(registrations) from campaign list - Extract _calc_change to module-level in partner_stats_service - Add composite indexes for stats queries on Subscription, Transaction, SubscriptionConversion, and TrafficPurchase models
241 lines
6.1 KiB
Python
241 lines
6.1 KiB
Python
"""Partner system schemas for cabinet."""
|
|
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
# ==================== User-facing ====================
|
|
|
|
|
|
class PartnerApplicationRequest(BaseModel):
|
|
"""Request to apply for partner status."""
|
|
|
|
company_name: str | None = Field(None, max_length=255)
|
|
website_url: str | None = Field(None, max_length=500)
|
|
telegram_channel: str | None = Field(None, max_length=255)
|
|
description: str | None = Field(None, max_length=2000)
|
|
expected_monthly_referrals: int | None = Field(None, ge=0, le=2_000_000_000)
|
|
desired_commission_percent: int | None = Field(None, ge=1, le=100)
|
|
|
|
|
|
class PartnerApplicationInfo(BaseModel):
|
|
"""Application info for the user."""
|
|
|
|
id: int
|
|
status: str
|
|
company_name: str | None = None
|
|
website_url: str | None = None
|
|
telegram_channel: str | None = None
|
|
description: str | None = None
|
|
expected_monthly_referrals: int | None = None
|
|
desired_commission_percent: int | None = None
|
|
admin_comment: str | None = None
|
|
approved_commission_percent: int | None = None
|
|
created_at: datetime
|
|
processed_at: datetime | None = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class PartnerCampaignInfo(BaseModel):
|
|
"""Campaign info visible to the partner."""
|
|
|
|
id: int
|
|
name: str
|
|
start_parameter: str
|
|
bonus_type: str
|
|
balance_bonus_kopeks: int = 0
|
|
subscription_duration_days: int | None = None
|
|
subscription_traffic_gb: int | None = None
|
|
deep_link: str | None = None
|
|
web_link: str | None = None
|
|
# Per-campaign statistics
|
|
registrations_count: int = 0
|
|
referrals_count: int = 0
|
|
earnings_kopeks: int = 0
|
|
|
|
|
|
class PartnerStatusResponse(BaseModel):
|
|
"""Partner status for current user."""
|
|
|
|
partner_status: str
|
|
commission_percent: int | None = None
|
|
latest_application: PartnerApplicationInfo | None = None
|
|
campaigns: list[PartnerCampaignInfo] = []
|
|
|
|
|
|
# ==================== Campaign detailed stats ====================
|
|
|
|
|
|
class DailyStatItem(BaseModel):
|
|
"""Single day of campaign stats."""
|
|
|
|
date: str
|
|
referrals_count: int = 0
|
|
earnings_kopeks: int = 0
|
|
|
|
|
|
class PeriodStats(BaseModel):
|
|
"""Stats for a single period."""
|
|
|
|
days: int
|
|
referrals_count: int = 0
|
|
earnings_kopeks: int = 0
|
|
|
|
|
|
class PeriodChange(BaseModel):
|
|
"""Change metrics between periods."""
|
|
|
|
absolute: int = 0
|
|
percent: float = 0.0
|
|
trend: str = 'stable'
|
|
|
|
|
|
class PeriodComparison(BaseModel):
|
|
"""Comparison between current and previous period."""
|
|
|
|
current: PeriodStats
|
|
previous: PeriodStats
|
|
referrals_change: PeriodChange
|
|
earnings_change: PeriodChange
|
|
|
|
|
|
class CampaignReferralItem(BaseModel):
|
|
"""Referral user in campaign stats."""
|
|
|
|
id: int
|
|
full_name: str
|
|
created_at: datetime
|
|
has_paid: bool = False
|
|
is_active: bool = False
|
|
total_earnings_kopeks: int = 0
|
|
|
|
|
|
class PartnerCampaignDetailedStats(BaseModel):
|
|
"""Detailed stats for a single campaign."""
|
|
|
|
campaign_id: int
|
|
campaign_name: str
|
|
# Summary
|
|
registrations_count: int = 0
|
|
referrals_count: int = 0
|
|
earnings_kopeks: int = 0
|
|
conversion_rate: float = 0.0
|
|
# Period earnings
|
|
earnings_today: int = 0
|
|
earnings_week: int = 0
|
|
earnings_month: int = 0
|
|
# Daily chart (30 days)
|
|
daily_stats: list[DailyStatItem] = []
|
|
# Period comparison (this week vs last week)
|
|
period_comparison: PeriodComparison
|
|
# Top referrals
|
|
top_referrals: list[CampaignReferralItem] = []
|
|
|
|
|
|
# ==================== Admin-facing ====================
|
|
|
|
|
|
class AdminPartnerApplicationItem(BaseModel):
|
|
"""Partner application in admin list."""
|
|
|
|
id: int
|
|
user_id: int
|
|
username: str | None = None
|
|
first_name: str | None = None
|
|
telegram_id: int | None = None
|
|
company_name: str | None = None
|
|
website_url: str | None = None
|
|
telegram_channel: str | None = None
|
|
description: str | None = None
|
|
expected_monthly_referrals: int | None = None
|
|
desired_commission_percent: int | None = None
|
|
status: str
|
|
admin_comment: str | None = None
|
|
approved_commission_percent: int | None = None
|
|
created_at: datetime
|
|
processed_at: datetime | None = None
|
|
|
|
|
|
class AdminPartnerApplicationsResponse(BaseModel):
|
|
"""List of partner applications."""
|
|
|
|
items: list[AdminPartnerApplicationItem]
|
|
total: int
|
|
|
|
|
|
class AdminApproveRequest(BaseModel):
|
|
"""Request to approve a partner application."""
|
|
|
|
commission_percent: int = Field(..., ge=1, le=100)
|
|
comment: str | None = Field(None, max_length=2000)
|
|
|
|
|
|
class AdminRejectRequest(BaseModel):
|
|
"""Request to reject a partner application."""
|
|
|
|
comment: str | None = Field(None, max_length=2000)
|
|
|
|
|
|
class AdminPartnerItem(BaseModel):
|
|
"""Partner in admin list."""
|
|
|
|
user_id: int
|
|
username: str | None = None
|
|
first_name: str | None = None
|
|
telegram_id: int | None = None
|
|
commission_percent: int | None = None
|
|
total_referrals: int = 0
|
|
total_earnings_kopeks: int = 0
|
|
balance_kopeks: int = 0
|
|
partner_status: str
|
|
created_at: datetime
|
|
|
|
|
|
class AdminPartnerListResponse(BaseModel):
|
|
"""List of partners for admin."""
|
|
|
|
items: list[AdminPartnerItem]
|
|
total: int
|
|
|
|
|
|
class CampaignSummary(BaseModel):
|
|
"""Campaign summary for partner detail."""
|
|
|
|
id: int
|
|
name: str
|
|
start_parameter: str
|
|
is_active: bool
|
|
registrations_count: int = 0
|
|
referrals_count: int = 0
|
|
earnings_kopeks: int = 0
|
|
|
|
|
|
class AdminPartnerDetailResponse(BaseModel):
|
|
"""Detailed partner info for admin."""
|
|
|
|
user_id: int
|
|
username: str | None = None
|
|
first_name: str | None = None
|
|
telegram_id: int | None = None
|
|
commission_percent: int | None = None
|
|
partner_status: str
|
|
balance_kopeks: int = 0
|
|
total_referrals: int = 0
|
|
paid_referrals: int = 0
|
|
active_referrals: int = 0
|
|
earnings_all_time: int = 0
|
|
earnings_today: int = 0
|
|
earnings_week: int = 0
|
|
earnings_month: int = 0
|
|
conversion_to_paid: float = 0.0
|
|
campaigns: list[CampaignSummary] = []
|
|
created_at: datetime
|
|
|
|
|
|
class AdminUpdateCommissionRequest(BaseModel):
|
|
"""Request to update partner commission."""
|
|
|
|
commission_percent: int = Field(..., ge=1, le=100)
|