"""Listing request / response schemas.""" from __future__ import annotations from datetime import datetime from typing import Optional from pydantic import BaseModel, Field, field_validator import math class ListingBase(BaseModel): title: str = Field(..., min_length=1, max_length=255) description: str = Field(..., min_length=1, max_length=5000) price: float = Field(..., gt=0) category_id: str location: str = Field(..., min_length=1, max_length=255) images: list[str] = Field(default_factory=list, max_length=20) listing_type: str = Field(default="sale", pattern="^(sale|rent)$") condition: Optional[str] = Field(None, pattern="^(new|used|refurbished)$") negotiable: bool = False @field_validator("price") @classmethod def price_must_be_finite(cls, v: float) -> float: if not math.isfinite(v): raise ValueError("price must be a finite number") return v @field_validator("images") @classmethod def images_must_be_http(cls, v: list) -> list: for url in v: if not (url.startswith("http://") or url.startswith("https://")): raise ValueError(f"Image URL must start with http:// or https://: {url!r}") return v class ListingCreate(ListingBase): pass class ListingUpdate(BaseModel): title: Optional[str] = Field(None, min_length=1, max_length=255) description: Optional[str] = Field(None, min_length=1, max_length=5000) price: Optional[float] = Field(None, ge=0) category_id: Optional[str] = None location: Optional[str] = Field(None, min_length=1, max_length=255) images: Optional[list[str]] = Field(None, max_length=20) listing_type: Optional[str] = Field(None, pattern="^(sale|rent)$") condition: Optional[str] = Field(None, pattern="^(new|used|refurbished)$") negotiable: Optional[bool] = None class ListingStatusUpdate(BaseModel): status: str = Field(..., pattern="^(approved|rejected)$") rejection_reason: Optional[str] = Field(None, max_length=1000) class ListingResponse(BaseModel): id: str title: str description: str price: float images: list[str] status: str agency_id: str category_id: str location: str listing_type: str condition: Optional[str] = None negotiable: bool views_count: int = 0 rejection_reason: Optional[str] = None created_at: datetime updated_at: Optional[datetime] = None # Joined fields (optional) agency_name: Optional[str] = None category_name: Optional[str] = None model_config = {"from_attributes": True} class ListingListResponse(BaseModel): listings: list[ListingResponse] total: int page: int page_size: int class ListingSearchParams(BaseModel): search: Optional[str] = None category: Optional[str] = None min_price: Optional[float] = None max_price: Optional[float] = None location: Optional[str] = None listing_type: Optional[str] = None condition: Optional[str] = None sort_by: str = Field(default="newest", pattern="^(newest|oldest|price_asc|price_desc|popular)$") status: Optional[str] = None page: int = Field(default=1, ge=1) page_size: int = Field(default=20, ge=1, le=100)