274 lines
6.4 KiB
Python
274 lines
6.4 KiB
Python
"""Pydantic schemas for API request/response models."""
|
|
|
|
from datetime import date, datetime, time
|
|
from typing import Generic, TypeVar
|
|
|
|
from pydantic import BaseModel, EmailStr, field_validator
|
|
|
|
from models import BitcoinTransferMethod, InviteStatus, Permission
|
|
|
|
|
|
class UserCredentials(BaseModel):
|
|
"""Base model for user email/password."""
|
|
|
|
email: EmailStr
|
|
password: str
|
|
|
|
|
|
UserCreate = UserCredentials
|
|
UserLogin = UserCredentials
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
"""Response model for authenticated user info."""
|
|
|
|
id: int
|
|
email: str
|
|
roles: list[str]
|
|
permissions: list[str]
|
|
|
|
|
|
class RegisterWithInvite(BaseModel):
|
|
"""Request model for registration with invite."""
|
|
|
|
email: EmailStr
|
|
password: str
|
|
invite_identifier: str
|
|
|
|
|
|
RecordT = TypeVar("RecordT", bound=BaseModel)
|
|
|
|
|
|
class PaginatedResponse(BaseModel, Generic[RecordT]):
|
|
"""Generic paginated response wrapper."""
|
|
|
|
records: list[RecordT]
|
|
total: int
|
|
page: int
|
|
per_page: int
|
|
total_pages: int
|
|
|
|
|
|
class ProfileResponse(BaseModel):
|
|
"""Response model for profile data."""
|
|
|
|
contact_email: str | None
|
|
telegram: str | None
|
|
signal: str | None
|
|
nostr_npub: str | None
|
|
godfather_email: str | None = None
|
|
|
|
|
|
class ProfileUpdate(BaseModel):
|
|
"""Request model for updating profile."""
|
|
|
|
contact_email: str | None = None
|
|
telegram: str | None = None
|
|
signal: str | None = None
|
|
nostr_npub: str | None = None
|
|
|
|
|
|
class InviteCheckResponse(BaseModel):
|
|
"""Response for invite check endpoint."""
|
|
|
|
valid: bool
|
|
status: str | None = None
|
|
error: str | None = None
|
|
|
|
|
|
class InviteCreate(BaseModel):
|
|
"""Request model for creating an invite."""
|
|
|
|
godfather_id: int
|
|
|
|
|
|
class InviteResponse(BaseModel):
|
|
"""Response model for invite data (admin view)."""
|
|
|
|
id: int
|
|
identifier: str
|
|
godfather_id: int
|
|
godfather_email: str
|
|
status: str
|
|
used_by_id: int | None
|
|
used_by_email: str | None
|
|
created_at: datetime
|
|
spent_at: datetime | None
|
|
revoked_at: datetime | None
|
|
|
|
|
|
class UserInviteResponse(BaseModel):
|
|
"""Response model for a user's invite (simpler than admin view)."""
|
|
|
|
id: int
|
|
identifier: str
|
|
status: str
|
|
used_by_email: str | None
|
|
created_at: datetime
|
|
spent_at: datetime | None
|
|
|
|
|
|
PaginatedInviteRecords = PaginatedResponse[InviteResponse]
|
|
|
|
|
|
class AdminUserResponse(BaseModel):
|
|
"""Minimal user info for admin dropdowns."""
|
|
|
|
id: int
|
|
email: str
|
|
|
|
|
|
# =============================================================================
|
|
# Availability Schemas
|
|
# =============================================================================
|
|
|
|
|
|
class TimeSlot(BaseModel):
|
|
"""A single time slot (start and end time)."""
|
|
|
|
start_time: time
|
|
end_time: time
|
|
|
|
@field_validator("start_time", "end_time")
|
|
@classmethod
|
|
def validate_15min_boundary(cls, v: time) -> time:
|
|
"""Ensure times are on 15-minute boundaries."""
|
|
if v.minute not in (0, 15, 30, 45):
|
|
raise ValueError("Time must be on 15-minute boundary (:00, :15, :30, :45)")
|
|
if v.second != 0 or v.microsecond != 0:
|
|
raise ValueError("Time must not have seconds or microseconds")
|
|
return v
|
|
|
|
|
|
class AvailabilityDay(BaseModel):
|
|
"""Availability for a single day."""
|
|
|
|
date: date
|
|
slots: list[TimeSlot]
|
|
|
|
|
|
class AvailabilityResponse(BaseModel):
|
|
"""Response model for availability query."""
|
|
|
|
days: list[AvailabilityDay]
|
|
|
|
|
|
class SetAvailabilityRequest(BaseModel):
|
|
"""Request to set availability for a specific date."""
|
|
|
|
date: date
|
|
slots: list[TimeSlot]
|
|
|
|
|
|
class CopyAvailabilityRequest(BaseModel):
|
|
"""Request to copy availability from one day to others."""
|
|
|
|
source_date: date
|
|
target_dates: list[date]
|
|
|
|
|
|
# =============================================================================
|
|
# Booking Schemas
|
|
# =============================================================================
|
|
|
|
|
|
# =============================================================================
|
|
# Price History Schemas
|
|
# =============================================================================
|
|
|
|
|
|
class PriceHistoryResponse(BaseModel):
|
|
"""Response model for a price history record."""
|
|
|
|
id: int
|
|
source: str
|
|
pair: str
|
|
price: float
|
|
timestamp: datetime
|
|
created_at: datetime
|
|
|
|
|
|
# =============================================================================
|
|
# Exchange Schemas
|
|
# =============================================================================
|
|
|
|
|
|
class ExchangeRequest(BaseModel):
|
|
"""Request to create an exchange trade."""
|
|
|
|
slot_start: datetime
|
|
direction: str # "buy" or "sell"
|
|
eur_amount: int # EUR cents (e.g., 10000 = €100)
|
|
|
|
|
|
class ExchangeResponse(BaseModel):
|
|
"""Response model for an exchange trade."""
|
|
|
|
id: int
|
|
user_id: int
|
|
user_email: str
|
|
slot_start: datetime
|
|
slot_end: datetime
|
|
direction: str
|
|
eur_amount: int # EUR cents
|
|
sats_amount: int # Satoshis
|
|
market_price_eur: float
|
|
agreed_price_eur: float
|
|
premium_percentage: int
|
|
status: str
|
|
created_at: datetime
|
|
cancelled_at: datetime | None
|
|
completed_at: datetime | None
|
|
|
|
|
|
class ExchangeUserContact(BaseModel):
|
|
"""User contact info for admin view."""
|
|
|
|
email: str
|
|
contact_email: str | None
|
|
telegram: str | None
|
|
signal: str | None
|
|
nostr_npub: str | None
|
|
|
|
|
|
class AdminExchangeResponse(BaseModel):
|
|
"""Response model for admin exchange view (includes user contact)."""
|
|
|
|
id: int
|
|
user_id: int
|
|
user_email: str
|
|
user_contact: ExchangeUserContact
|
|
slot_start: datetime
|
|
slot_end: datetime
|
|
direction: str
|
|
eur_amount: int
|
|
sats_amount: int
|
|
market_price_eur: float
|
|
agreed_price_eur: float
|
|
premium_percentage: int
|
|
status: str
|
|
created_at: datetime
|
|
cancelled_at: datetime | None
|
|
completed_at: datetime | None
|
|
|
|
|
|
PaginatedExchanges = PaginatedResponse[ExchangeResponse]
|
|
PaginatedAdminExchanges = PaginatedResponse[AdminExchangeResponse]
|
|
|
|
|
|
# =============================================================================
|
|
# Meta/Constants Schemas
|
|
# =============================================================================
|
|
|
|
|
|
class ConstantsResponse(BaseModel):
|
|
"""Response model for shared constants.
|
|
|
|
Note: Using actual enum types ensures OpenAPI schema includes enum values,
|
|
allowing frontend type generation to produce matching TypeScript enums.
|
|
"""
|
|
|
|
permissions: list[Permission]
|
|
roles: list[str]
|
|
invite_statuses: list[InviteStatus]
|
|
bitcoin_transfer_methods: list[BitcoinTransferMethod]
|