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, )