from __future__ import annotations from datetime import datetime from pydantic import BaseModel, Field, field_validator, model_validator class PollOptionCreate(BaseModel): text: str = Field(..., min_length=1, max_length=500) @field_validator('text') @classmethod def strip_text(cls, value: str) -> str: text = value.strip() if not text: raise ValueError('Option text cannot be empty') return text class PollQuestionCreate(BaseModel): text: str = Field(..., min_length=1, max_length=1000) options: list[PollOptionCreate] = Field(..., min_length=2) @field_validator('text') @classmethod def strip_question_text(cls, value: str) -> str: text = value.strip() if not text: raise ValueError('Question text cannot be empty') return text @field_validator('options') @classmethod def validate_options(cls, value: list[PollOptionCreate]) -> list[PollOptionCreate]: seen: set[str] = set() for option in value: normalized = option.text.lower() if normalized in seen: raise ValueError('Option texts must be unique within a question') seen.add(normalized) return value class PollCreateRequest(BaseModel): title: str = Field(..., min_length=1, max_length=255) description: str | None = Field(default=None, max_length=4000) reward_enabled: bool = False reward_amount_kopeks: int = Field(default=0, ge=0, le=1_000_000_000) questions: list[PollQuestionCreate] = Field(..., min_length=1) @field_validator('title') @classmethod def strip_title(cls, value: str) -> str: title = value.strip() if not title: raise ValueError('Title cannot be empty') return title @field_validator('description') @classmethod def normalize_description(cls, value: str | None) -> str | None: if value is None: return None description = value.strip() return description or None @model_validator(mode='after') def validate_reward(self) -> PollCreateRequest: if self.reward_enabled and self.reward_amount_kopeks <= 0: raise ValueError('Reward amount must be positive when rewards are enabled') if not self.reward_enabled: self.reward_amount_kopeks = 0 return self class PollQuestionOptionResponse(BaseModel): id: int text: str order: int class PollQuestionResponse(BaseModel): id: int text: str order: int options: list[PollQuestionOptionResponse] class PollSummaryResponse(BaseModel): id: int title: str description: str | None reward_enabled: bool reward_amount_kopeks: int reward_amount_rubles: float questions_count: int responses_count: int created_at: datetime updated_at: datetime class PollDetailResponse(BaseModel): id: int title: str description: str | None reward_enabled: bool reward_amount_kopeks: int reward_amount_rubles: float questions: list[PollQuestionResponse] created_at: datetime updated_at: datetime class PollListResponse(BaseModel): items: list[PollSummaryResponse] total: int limit: int offset: int class PollOptionStats(BaseModel): id: int text: str count: int class PollQuestionStats(BaseModel): id: int text: str order: int options: list[PollOptionStats] class PollStatisticsResponse(BaseModel): poll_id: int poll_title: str total_responses: int completed_responses: int reward_sum_kopeks: int reward_sum_rubles: float questions: list[PollQuestionStats] class PollAnswerResponse(BaseModel): question_id: int | None question_text: str | None option_id: int | None option_text: str | None created_at: datetime class PollUserResponse(BaseModel): id: int user_id: int | None user_telegram_id: int | None user_username: str | None sent_at: datetime started_at: datetime | None completed_at: datetime | None reward_given: bool reward_amount_kopeks: int reward_amount_rubles: float answers: list[PollAnswerResponse] class PollResponsesListResponse(BaseModel): items: list[PollUserResponse] total: int limit: int offset: int class PollSendRequest(BaseModel): target: str = Field( ..., description=('Аудитория для отправки опроса (например: all, active, trial, custom_today и т.д.)'), max_length=100, ) class PollSendResponse(BaseModel): poll_id: int target: str sent: int failed: int skipped: int total: int