Phase 1 Database Implementation (DB-001 through DB-012): Models: - User: OAuth support (Google/Discord), premium subscriptions - Collection: Card ownership with CardSource enum - Deck: JSONB cards/energy_cards, validation state - CampaignProgress: One-to-one with User, medals/NPCs as JSONB - ActiveGame: In-progress games with GameType enum - GameHistory: Completed games with EndReason enum, replay data Infrastructure: - Alembic migrations with sync psycopg2 (avoids async issues) - Docker Compose for Postgres (5433) and Redis (6380) - App config with Pydantic settings - Redis client helper Test Infrastructure: - 68 database tests (47 model + 21 relationship) - Async factory pattern for test data creation - Sync TRUNCATE cleanup (solves pytest-asyncio event loop mismatch) - Uses dev containers instead of testcontainers for reliability Key technical decisions: - passive_deletes=True for ON DELETE SET NULL relationships - NullPool for test sessions (no connection reuse) - expire_on_commit=False with manual expire() for relationship tests
136 lines
3.6 KiB
Python
136 lines
3.6 KiB
Python
"""Collection model for Mantimon TCG.
|
|
|
|
This module defines the Collection model for tracking player card ownership.
|
|
Each entry represents a card the player owns, with quantity and source tracking.
|
|
|
|
Example:
|
|
collection_entry = Collection(
|
|
user_id=user.id,
|
|
card_definition_id="pikachu_base_001",
|
|
quantity=3,
|
|
source=CardSource.BOOSTER
|
|
)
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import (
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy import (
|
|
Enum as SQLEnum,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.db.base import Base
|
|
|
|
if TYPE_CHECKING:
|
|
from app.db.models.user import User
|
|
|
|
|
|
class CardSource(str, Enum):
|
|
"""Source of how a card was obtained.
|
|
|
|
Attributes:
|
|
STARTER: From a starter deck selection.
|
|
BOOSTER: From opening a booster pack.
|
|
REWARD: From winning matches or achievements.
|
|
PURCHASE: From direct purchase with Mantibucks.
|
|
TRADE: From trading with another player.
|
|
GIFT: From promotional events or gifts.
|
|
"""
|
|
|
|
STARTER = "starter"
|
|
BOOSTER = "booster"
|
|
REWARD = "reward"
|
|
PURCHASE = "purchase"
|
|
TRADE = "trade"
|
|
GIFT = "gift"
|
|
|
|
|
|
class Collection(Base):
|
|
"""Player card collection entry.
|
|
|
|
Tracks ownership of cards with quantity and source information.
|
|
Each (user_id, card_definition_id) pair is unique - quantities are aggregated.
|
|
|
|
Attributes:
|
|
id: Unique identifier (UUID).
|
|
user_id: Foreign key to the owning user.
|
|
card_definition_id: ID of the card definition (e.g., "pikachu_base_001").
|
|
quantity: Number of copies owned (1+).
|
|
source: How the first copy was obtained.
|
|
obtained_at: When the card was first added to collection.
|
|
created_at: Record creation timestamp.
|
|
updated_at: Record update timestamp.
|
|
|
|
Relationships:
|
|
user: The user who owns this card.
|
|
"""
|
|
|
|
__tablename__ = "collections"
|
|
|
|
# Ownership
|
|
user_id: Mapped[UUID] = mapped_column(
|
|
ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
doc="Foreign key to owning user",
|
|
)
|
|
|
|
# Card reference (string ID matching card JSON data)
|
|
card_definition_id: Mapped[str] = mapped_column(
|
|
String(100),
|
|
nullable=False,
|
|
doc="Card definition ID (e.g., 'pikachu_base_001')",
|
|
)
|
|
|
|
# Quantity tracking
|
|
quantity: Mapped[int] = mapped_column(
|
|
Integer,
|
|
default=1,
|
|
nullable=False,
|
|
doc="Number of copies owned",
|
|
)
|
|
|
|
# Source tracking
|
|
source: Mapped[CardSource] = mapped_column(
|
|
SQLEnum(CardSource, name="card_source", create_constraint=True),
|
|
nullable=False,
|
|
doc="How the card was obtained",
|
|
)
|
|
|
|
# When first obtained
|
|
obtained_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=datetime.utcnow,
|
|
nullable=False,
|
|
doc="When card was first added to collection",
|
|
)
|
|
|
|
# Relationship
|
|
user: Mapped["User"] = relationship(
|
|
"User",
|
|
back_populates="collection",
|
|
)
|
|
|
|
# Constraints and indexes
|
|
__table_args__ = (
|
|
UniqueConstraint("user_id", "card_definition_id", name="uq_collection_user_card"),
|
|
Index("ix_collection_card", "card_definition_id"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"<Collection(user_id={self.user_id!r}, "
|
|
f"card={self.card_definition_id!r}, qty={self.quantity})>"
|
|
)
|