arbret/backend/models/exchange.py
2025-12-26 20:04:46 +01:00

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
)