refactors
This commit is contained in:
parent
4e1a339432
commit
82c4d0168e
28 changed files with 1042 additions and 782 deletions
88
backend/schemas/__init__.py
Normal file
88
backend/schemas/__init__.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Export pagination
|
||||
# Export auth schemas
|
||||
from .auth import RegisterWithInvite, UserCreate, UserCredentials, UserLogin
|
||||
|
||||
# Export availability schemas
|
||||
from .availability import (
|
||||
AvailabilityDay,
|
||||
AvailabilityResponse,
|
||||
CopyAvailabilityRequest,
|
||||
SetAvailabilityRequest,
|
||||
TimeSlot,
|
||||
)
|
||||
|
||||
# Export exchange schemas
|
||||
from .exchange import (
|
||||
AdminExchangeResponse,
|
||||
AvailableSlotsResponse,
|
||||
BookableSlot,
|
||||
ExchangeRequest,
|
||||
ExchangeResponse,
|
||||
ExchangeUserContact,
|
||||
PaginatedAdminExchanges,
|
||||
PaginatedExchanges,
|
||||
)
|
||||
|
||||
# Export invite schemas
|
||||
from .invite import (
|
||||
InviteCheckResponse,
|
||||
InviteCreate,
|
||||
InviteResponse,
|
||||
PaginatedInviteRecords,
|
||||
UserInviteResponse,
|
||||
)
|
||||
|
||||
# Export meta schemas
|
||||
from .meta import ConstantsResponse
|
||||
from .pagination import PaginatedResponse, RecordT
|
||||
|
||||
# Export price schemas
|
||||
from .price import (
|
||||
ExchangeConfigResponse,
|
||||
ExchangePriceResponse,
|
||||
PriceHistoryResponse,
|
||||
PriceResponse,
|
||||
)
|
||||
|
||||
# Export profile schemas
|
||||
from .profile import ProfileResponse, ProfileUpdate
|
||||
|
||||
# Export user schemas
|
||||
from .user import AdminUserResponse, UserResponse, UserSearchResult
|
||||
|
||||
__all__ = [
|
||||
"AdminExchangeResponse",
|
||||
"AdminUserResponse",
|
||||
"AvailabilityDay",
|
||||
"AvailabilityResponse",
|
||||
"AvailableSlotsResponse",
|
||||
"BookableSlot",
|
||||
"ConstantsResponse",
|
||||
"CopyAvailabilityRequest",
|
||||
"ExchangeConfigResponse",
|
||||
"ExchangePriceResponse",
|
||||
"ExchangeRequest",
|
||||
"ExchangeResponse",
|
||||
"ExchangeUserContact",
|
||||
"InviteCheckResponse",
|
||||
"InviteCreate",
|
||||
"InviteResponse",
|
||||
"PaginatedAdminExchanges",
|
||||
"PaginatedExchanges",
|
||||
"PaginatedInviteRecords",
|
||||
"PaginatedResponse",
|
||||
"PriceHistoryResponse",
|
||||
"PriceResponse",
|
||||
"ProfileResponse",
|
||||
"ProfileUpdate",
|
||||
"RecordT",
|
||||
"RegisterWithInvite",
|
||||
"SetAvailabilityRequest",
|
||||
"TimeSlot",
|
||||
"UserCreate",
|
||||
"UserCredentials",
|
||||
"UserInviteResponse",
|
||||
"UserLogin",
|
||||
"UserResponse",
|
||||
"UserSearchResult",
|
||||
]
|
||||
20
backend/schemas/auth.py
Normal file
20
backend/schemas/auth.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
|
||||
class UserCredentials(BaseModel):
|
||||
"""Base model for user email/password."""
|
||||
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
UserCreate = UserCredentials
|
||||
UserLogin = UserCredentials
|
||||
|
||||
|
||||
class RegisterWithInvite(BaseModel):
|
||||
"""Request model for registration with invite."""
|
||||
|
||||
email: EmailStr
|
||||
password: str
|
||||
invite_identifier: str
|
||||
47
backend/schemas/availability.py
Normal file
47
backend/schemas/availability.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from datetime import date, time
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
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]
|
||||
87
backend/schemas/exchange.py
Normal file
87
backend/schemas/exchange.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .pagination import PaginatedResponse
|
||||
|
||||
|
||||
class ExchangeRequest(BaseModel):
|
||||
"""Request to create an exchange trade."""
|
||||
|
||||
slot_start: datetime
|
||||
direction: str # "buy" or "sell"
|
||||
bitcoin_transfer_method: str # "onchain" or "lightning"
|
||||
eur_amount: int # EUR cents (e.g., 10000 = €100)
|
||||
|
||||
|
||||
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 ExchangeResponse(BaseModel):
|
||||
"""Response model for an exchange trade."""
|
||||
|
||||
id: int # Keep for backward compatibility, but prefer public_id
|
||||
public_id: str # UUID as string
|
||||
user_id: int
|
||||
user_email: str
|
||||
slot_start: datetime
|
||||
slot_end: datetime
|
||||
direction: str
|
||||
bitcoin_transfer_method: 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 AdminExchangeResponse(BaseModel):
|
||||
"""Response model for admin exchange view (includes user contact)."""
|
||||
|
||||
id: int # Keep for backward compatibility, but prefer public_id
|
||||
public_id: str # UUID as string
|
||||
user_id: int
|
||||
user_email: str
|
||||
user_contact: ExchangeUserContact
|
||||
slot_start: datetime
|
||||
slot_end: datetime
|
||||
direction: str
|
||||
bitcoin_transfer_method: 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]
|
||||
|
||||
|
||||
class BookableSlot(BaseModel):
|
||||
"""A single bookable time slot."""
|
||||
|
||||
start_time: datetime
|
||||
end_time: datetime
|
||||
|
||||
|
||||
class AvailableSlotsResponse(BaseModel):
|
||||
"""Response containing available slots for a date."""
|
||||
|
||||
date: date
|
||||
slots: list[BookableSlot]
|
||||
48
backend/schemas/invite.py
Normal file
48
backend/schemas/invite.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .pagination import PaginatedResponse
|
||||
|
||||
|
||||
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]
|
||||
16
backend/schemas/meta.py
Normal file
16
backend/schemas/meta.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from models import BitcoinTransferMethod, InviteStatus, Permission
|
||||
|
||||
|
||||
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]
|
||||
15
backend/schemas/pagination.py
Normal file
15
backend/schemas/pagination.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
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
|
||||
44
backend/schemas/price.py
Normal file
44
backend/schemas/price.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PriceHistoryResponse(BaseModel):
|
||||
"""Response model for a price history record."""
|
||||
|
||||
id: int
|
||||
source: str
|
||||
pair: str
|
||||
price: float
|
||||
timestamp: datetime
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ExchangeConfigResponse(BaseModel):
|
||||
"""Exchange configuration for the frontend."""
|
||||
|
||||
eur_min: int
|
||||
eur_max: int
|
||||
eur_increment: int
|
||||
premium_percentage: int
|
||||
|
||||
|
||||
class PriceResponse(BaseModel):
|
||||
"""Current BTC/EUR price for trading.
|
||||
|
||||
Note: The actual agreed price depends on trade direction (buy/sell)
|
||||
and is calculated by the frontend using market_price and premium_percentage.
|
||||
"""
|
||||
|
||||
market_price: float # Raw price from exchange
|
||||
premium_percentage: int
|
||||
timestamp: datetime
|
||||
is_stale: bool
|
||||
|
||||
|
||||
class ExchangePriceResponse(BaseModel):
|
||||
"""Combined price and configuration response."""
|
||||
|
||||
price: PriceResponse | None # None if price fetch failed
|
||||
config: ExchangeConfigResponse
|
||||
error: str | None = None
|
||||
20
backend/schemas/profile.py
Normal file
20
backend/schemas/profile.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
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
|
||||
24
backend/schemas/user.py
Normal file
24
backend/schemas/user.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""Response model for authenticated user info."""
|
||||
|
||||
id: int
|
||||
email: str
|
||||
roles: list[str]
|
||||
permissions: list[str]
|
||||
|
||||
|
||||
class AdminUserResponse(BaseModel):
|
||||
"""Minimal user info for admin dropdowns."""
|
||||
|
||||
id: int
|
||||
email: str
|
||||
|
||||
|
||||
class UserSearchResult(BaseModel):
|
||||
"""Result item for user search."""
|
||||
|
||||
id: int
|
||||
email: str
|
||||
Loading…
Add table
Add a link
Reference in a new issue