105 lines
4.3 KiB
Python
105 lines
4.3 KiB
Python
import enum
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from sqlalchemy import String, Integer, ForeignKey, DateTime, Enum, Boolean
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
class TicketStatus(str, enum.Enum):
|
|
AVAILABLE = "AVAILABLE"
|
|
LOCKED = "LOCKED"
|
|
PAID = "PAID"
|
|
SCANNED = "SCANNED"
|
|
REFUNDED = "REFUNDED"
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
email: Mapped[str] = mapped_column(String, unique=True, index=True)
|
|
hashed_password: Mapped[str] = mapped_column(String)
|
|
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
tickets: Mapped[list["Ticket"]] = relationship(back_populates="user")
|
|
|
|
class Tournament(Base):
|
|
__tablename__ = "tournaments"
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
title: Mapped[str] = mapped_column(String)
|
|
description: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
event_date: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
seats: Mapped[list["Seat"]] = relationship(back_populates="tournament")
|
|
|
|
class Seat(Base):
|
|
__tablename__ = "seats"
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
tournament_id: Mapped[int] = mapped_column(ForeignKey("tournaments.id"), index=True)
|
|
sector: Mapped[str] = mapped_column(String)
|
|
row: Mapped[int] = mapped_column(Integer)
|
|
number: Mapped[int] = mapped_column(Integer)
|
|
price: Mapped[int] = mapped_column(Integer)
|
|
|
|
tournament: Mapped["Tournament"] = relationship(back_populates="seats")
|
|
ticket: Mapped["Ticket"] = relationship(back_populates="seat", uselist=False)
|
|
|
|
class Ticket(Base):
|
|
__tablename__ = "tickets"
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
seat_id: Mapped[int] = mapped_column(ForeignKey("seats.id"), unique=True, index=True)
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
|
|
|
status: Mapped[TicketStatus] = mapped_column(
|
|
Enum(TicketStatus, name="ticket_status_enum", create_type=False),
|
|
default=TicketStatus.AVAILABLE,
|
|
index=True
|
|
)
|
|
idempotency_key: Mapped[str] = mapped_column(String, unique=True, nullable=True)
|
|
pdf_url: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
secret_token: Mapped[str | None] = mapped_column(
|
|
String, unique=True, index=True, nullable=True,
|
|
default=lambda: str(uuid.uuid4()),
|
|
)
|
|
|
|
# --- Поля для эквайринга ---
|
|
payment_id: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
|
payment_url: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
# ---------------------------
|
|
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=lambda: datetime.now(timezone.utc),
|
|
onupdate=lambda: datetime.now(timezone.utc)
|
|
)
|
|
|
|
seat: Mapped["Seat"] = relationship(back_populates="ticket")
|
|
user: Mapped["User"] = relationship(back_populates="tickets")
|
|
|
|
|
|
class ActionLog(Base):
|
|
"""Audit trail: every mutating request is recorded here by AuditLogMiddleware."""
|
|
|
|
__tablename__ = "action_logs"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
|
|
# nullable — anonymous / unauthenticated requests (e.g. /api/auth/register)
|
|
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
|
|
|
# "POST /api/tickets/book", "DELETE /api/seats/42", etc.
|
|
action: Mapped[str] = mapped_column(String, nullable=False, index=True)
|
|
|
|
# request.client.host or X-Forwarded-For (behind Traefik)
|
|
ip_address: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
|
|
# Optional structured payload (response body excerpt, error detail, …)
|
|
details: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=lambda: datetime.now(timezone.utc),
|
|
index=True,
|
|
) |