83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
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
|
|
)
|