Initial commit

This commit is contained in:
belviskhoremk
2026-03-06 22:57:58 +00:00
commit c4d836a0f9
60 changed files with 5423 additions and 0 deletions

7
app/schemas/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
from app.schemas.auth import * # noqa: F401, F403
from app.schemas.user import * # noqa: F401, F403
from app.schemas.agency import * # noqa: F401, F403
from app.schemas.category import * # noqa: F401, F403
from app.schemas.listing import * # noqa: F401, F403
from app.schemas.message import * # noqa: F401, F403
from app.schemas.favorite import * # noqa: F401, F403

55
app/schemas/agency.py Normal file
View File

@@ -0,0 +1,55 @@
"""Agency request / response schemas."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, HttpUrl
class AgencyBase(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: str = Field(..., min_length=1, max_length=2000)
address: str = Field(..., min_length=1, max_length=500)
phone: str = Field(..., min_length=1, max_length=20)
email: EmailStr
website: Optional[str] = Field(None, max_length=500)
class AgencyCreate(AgencyBase):
pass
class AgencyUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = Field(None, min_length=1, max_length=2000)
address: Optional[str] = Field(None, min_length=1, max_length=500)
phone: Optional[str] = Field(None, min_length=1, max_length=20)
email: Optional[EmailStr] = None
website: Optional[str] = Field(None, max_length=500)
logo: Optional[str] = None
class AgencyResponse(BaseModel):
id: str
user_id: str
name: str
description: str
logo: Optional[str] = None
address: str
phone: str
email: str
website: Optional[str] = None
verified: bool
created_at: datetime
updated_at: Optional[datetime] = None
model_config = {"from_attributes": True}
class AgencyListResponse(BaseModel):
agencies: list[AgencyResponse]
total: int
page: int
page_size: int

57
app/schemas/auth.py Normal file
View File

@@ -0,0 +1,57 @@
"""Auth-related request / response schemas."""
from __future__ import annotations
from typing import Any, Dict
from pydantic import BaseModel, EmailStr, Field
# ── Requests ──────────────────────────────────────────────
class RegisterRequest(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8, max_length=128)
name: str = Field(..., min_length=1, max_length=255)
role: str = Field(default="visitor", pattern="^(visitor|agency)$")
class LoginRequest(BaseModel):
email: EmailStr
password: str
class RefreshTokenRequest(BaseModel):
refresh_token: str
class PasswordResetRequest(BaseModel):
email: EmailStr
class PasswordResetConfirm(BaseModel):
new_password: str = Field(..., min_length=8, max_length=128)
class ChangePasswordRequest(BaseModel):
current_password: str
new_password: str = Field(..., min_length=8, max_length=128)
# ── Responses ─────────────────────────────────────────────
class TokenResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
class RegisterResponse(BaseModel):
message: str
user: Dict[str, Any]
class MessageResponse(BaseModel):
message: str

44
app/schemas/category.py Normal file
View File

@@ -0,0 +1,44 @@
"""Category request / response schemas."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
class CategoryBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: str = Field(..., min_length=1, max_length=500)
icon: str = Field(default="tag", max_length=50)
slug: str = Field(..., min_length=1, max_length=100, pattern="^[a-z0-9]+(?:-[a-z0-9]+)*$")
class CategoryCreate(CategoryBase):
pass
class CategoryUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, min_length=1, max_length=500)
icon: Optional[str] = Field(None, max_length=50)
slug: Optional[str] = Field(None, min_length=1, max_length=100, pattern="^[a-z0-9]+(?:-[a-z0-9]+)*$")
class CategoryResponse(BaseModel):
id: str
name: str
description: str
icon: str
slug: str
listing_count: int = 0
created_at: datetime
updated_at: Optional[datetime] = None
model_config = {"from_attributes": True}
class CategoryListResponse(BaseModel):
categories: list[CategoryResponse]
total: int

25
app/schemas/favorite.py Normal file
View File

@@ -0,0 +1,25 @@
"""Favorite / wishlist schemas."""
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel
class FavoriteCreate(BaseModel):
listing_id: str
class FavoriteResponse(BaseModel):
id: str
user_id: str
listing_id: str
created_at: datetime
model_config = {"from_attributes": True}
class FavoriteListResponse(BaseModel):
favorites: list[FavoriteResponse]
total: int

102
app/schemas/listing.py Normal file
View File

@@ -0,0 +1,102 @@
"""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)

43
app/schemas/message.py Normal file
View File

@@ -0,0 +1,43 @@
"""Message / contact-form schemas."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class MessageCreate(BaseModel):
listing_id: str
name: str = Field(..., min_length=1, max_length=255)
email: EmailStr
phone: Optional[str] = Field(None, max_length=20)
message: str = Field(..., min_length=1, max_length=2000)
class MessageResponse(BaseModel):
id: str
listing_id: str
agency_id: str
name: str
email: str
phone: Optional[str] = None
message: str
read: bool
created_at: datetime
# Joined
listing_title: Optional[str] = None
model_config = {"from_attributes": True}
class MessageListResponse(BaseModel):
messages: list[MessageResponse]
total: int
page: int
page_size: int
class MessageMarkRead(BaseModel):
read: bool = True

64
app/schemas/payment.py Normal file
View File

@@ -0,0 +1,64 @@
"""Payment-related Pydantic schemas."""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, Optional
from pydantic import BaseModel
class PaymentInitiate(BaseModel):
type: str # 'subscription' | 'purchase'
plan: Optional[str] = None # 'monthly' | 'yearly' (subscription only)
listing_id: Optional[str] = None # purchase only
class PaymentResponse(BaseModel):
id: str
transaction_id: str
type: str
payer_id: Optional[str] = None
amount: float
currency: str
status: str
payment_method: Optional[str] = None
operator_id: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
created_at: datetime
paid_at: Optional[datetime] = None
class PaymentInitiateResponse(BaseModel):
payment_url: str
transaction_id: str
class PaymentReceiptResponse(BaseModel):
id: str
transaction_id: str
type: str
amount: float
currency: str
status: str
payment_method: Optional[str] = None
operator_id: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
created_at: datetime
paid_at: Optional[datetime] = None
# Enriched fields
payer_name: Optional[str] = None
payer_email: Optional[str] = None
plan_label: Optional[str] = None
listing_title: Optional[str] = None
class SubscriptionResponse(BaseModel):
id: str
agency_id: str
plan: str
status: str
starts_at: datetime
ends_at: datetime
payment_id: Optional[str] = None
created_at: datetime

46
app/schemas/user.py Normal file
View File

@@ -0,0 +1,46 @@
"""User request / response schemas."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class UserBase(BaseModel):
email: EmailStr
name: str = Field(..., min_length=1, max_length=255)
role: str = Field(default="visitor")
class UserCreate(UserBase):
password: str = Field(..., min_length=8, max_length=128)
class UserUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
email: Optional[EmailStr] = None
phone: Optional[str] = Field(None, max_length=20)
avatar_url: Optional[str] = None
class UserResponse(BaseModel):
id: str
email: str
name: str
role: str
verified: bool
phone: Optional[str] = None
avatar_url: Optional[str] = None
created_at: datetime
updated_at: Optional[datetime] = None
model_config = {"from_attributes": True}
class UserListResponse(BaseModel):
users: list[UserResponse]
total: int
page: int
page_size: int