import uuid from datetime import UTC, datetime from typing import TYPE_CHECKING from sqlalchemy import DateTime, Enum, Float, ForeignKey, Integer from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from database import Base from .enums import BitcoinTransferMethod, ExchangeStatus, TradeDirection if TYPE_CHECKING: from .user import User class Exchange(Base): """Bitcoin exchange trades booked by users.""" __tablename__ = "exchanges" # Note: No unique constraint on slot_start to allow cancelled bookings # to be replaced. Application-level check in create_exchange ensures only # one BOOKED trade per slot. For existing databases, manually drop the # constraint: ALTER TABLE exchanges DROP CONSTRAINT IF EXISTS # uq_exchange_slot_start; id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) public_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), nullable=False, unique=True, index=True, default=uuid.uuid4, ) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False, index=True ) user: Mapped["User"] = relationship("User", foreign_keys=[user_id], lazy="joined") # Slot timing slot_start: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, index=True ) slot_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) # Trade details direction: Mapped[TradeDirection] = mapped_column( Enum(TradeDirection), nullable=False ) bitcoin_transfer_method: Mapped[BitcoinTransferMethod] = mapped_column( Enum(BitcoinTransferMethod), nullable=False, default=BitcoinTransferMethod.ONCHAIN, ) eur_amount: Mapped[int] = mapped_column(Integer, nullable=False) # EUR cents sats_amount: Mapped[int] = mapped_column(Integer, nullable=False) # Satoshis # Price information (snapshot at booking time) market_price_eur: Mapped[float] = mapped_column( Float, nullable=False ) # EUR per BTC agreed_price_eur: Mapped[float] = mapped_column( Float, nullable=False ) # EUR per BTC with premium premium_percentage: Mapped[int] = mapped_column( Integer, nullable=False ) # e.g. 5 for 5% # Status status: Mapped[ExchangeStatus] = mapped_column( Enum(ExchangeStatus), nullable=False, default=ExchangeStatus.BOOKED ) # Timestamps created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(UTC) ) cancelled_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) completed_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True )