from datetime import datetime, timedelta from typing import Literal from pydantic import BaseModel, EmailStr, Field, ValidationInfo, field_validator LeadStatus = Literal["new", "assigned", "contacted", "planning", "won", "invalid"] class LoginIn(BaseModel): email: EmailStr password: str = Field(min_length=6) class PhoneLoginIn(BaseModel): code: str = Field(min_length=1, max_length=256) class LeadCreateIn(BaseModel): destination: str | None = None phone: str = Field(min_length=2, max_length=64) travelDate: datetime | None = None peopleCount: int | None = Field(default=None, gt=0) budgetMin: int | None = Field(default=None, ge=0) budgetMax: int | None = Field(default=None, ge=0) note: str | None = Field(default=None, max_length=1000) sourcePage: str | None = None @field_validator("phone") @classmethod def normalize_phone(cls, value: str) -> str: normalized = " ".join(value.strip().split()) if not normalized: raise ValueError("联系方式不能为空") return normalized @field_validator("travelDate", mode="before") @classmethod def parse_date_only_travel_date(cls, value): if isinstance(value, str) and len(value) == 10: try: return datetime.strptime(value, "%Y-%m-%d") except ValueError: return value return value class LeadStatusIn(BaseModel): status: LeadStatus class LeadQuery(BaseModel): status: LeadStatus | None = None sourcePage: str | None = None keyword: str | None = None createdFrom: datetime | None = None createdTo: datetime | None = None take: int = Field(default=100, ge=1, le=200) @field_validator("createdFrom", "createdTo", mode="before") @classmethod def parse_date_only_range(cls, value, info: ValidationInfo): if isinstance(value, str) and len(value) == 10: try: parsed = datetime.strptime(value, "%Y-%m-%d") return parsed + timedelta(days=1) if info.field_name == "createdTo" else parsed except ValueError: return value return value class SiteConfigPatchIn(BaseModel): title: str | None = None kicker: str | None = None subtitle: str | None = None image: str | None = None description: str | None = None steps: list[str] | None = None destinationLabel: str | None = None destinationPlaceholder: str | None = None phoneLabel: str | None = None phonePlaceholder: str | None = None noteLabel: str | None = None notePlaceholder: str | None = None submitLabel: str | None = None chips: list[str] | None = None targetType: str | None = None targetValue: str | None = None isActive: bool | None = None sortOrder: int | None = None class SiteConfigReorderIn(BaseModel): itemIds: list[str] = Field(default_factory=list)