From 8b883258ba5fb616242f9b6da9be55c18eaaac84 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 9 Feb 2026 14:25:58 +0100 Subject: [PATCH] feat: add database schema v1 --- .env | 6 +-- backend/app/main.py | 6 ++- backend/app/models.py | 114 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 backend/app/models.py diff --git a/.env b/.env index c60c381..8263c8f 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DB=card_game_db +POSTGRES_USER=cardgame +POSTGRES_PASSWORD=cardgame +POSTGRES_DB=cardgame diff --git a/backend/app/main.py b/backend/app/main.py index 53de31a..7d52dc8 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,11 @@ from fastapi import FastAPI, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import text -from app.database import get_db +from app.database import get_db, engine +from app import models + +# Create all tables +models.Base.metadata.create_all(bind=engine) app = FastAPI(title="Card Game Backend") diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..d42cf44 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,114 @@ +from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Enum, UniqueConstraint +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.database import Base +import uuid + +def generate_uuid(): + return str(uuid.uuid4()) + +class User(Base): + __tablename__ = "users" + id = Column(String, primary_key=True, default=generate_uuid) + email = Column(String, unique=True, nullable=False) + password_hash = Column(String, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + profile = relationship("UserProfile", back_populates="user", uselist=False) + wallet = relationship("Wallet", back_populates="user", uselist=False) + cards = relationship("UserCard", back_populates="user") + transactions = relationship("WalletTransaction", back_populates="user") + chest_opens = relationship("ChestOpen", back_populates="user") + +class UserProfile(Base): + __tablename__ = "user_profiles" + user_id = Column(String, ForeignKey("users.id"), primary_key=True) + nickname = Column(String, unique=True, nullable=False) + is_collection_public = Column(Boolean, default=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="profile") + +class Wallet(Base): + __tablename__ = "wallets" + user_id = Column(String, ForeignKey("users.id"), primary_key=True) + balance = Column(Integer, nullable=False, default=0) + + user = relationship("User", back_populates="wallet") + +class WalletTransaction(Base): + __tablename__ = "wallet_transactions" + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + type = Column(String, nullable=False) # CREDIT / DEBIT + amount = Column(Integer, nullable=False) + reason = Column(String, nullable=False) # REGISTER_BONUS, CHEST_OPEN, etc. + reference_id = Column(String, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="transactions") + +class Card(Base): + __tablename__ = "cards" + id = Column(String, primary_key=True, default=generate_uuid) + name = Column(String, nullable=False) + rarity = Column(String, nullable=False) # COMMON, RARE, LEGENDARY + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + owners = relationship("UserCard", back_populates="card") + +class Chest(Base): + __tablename__ = "chests" + id = Column(String, primary_key=True, default=generate_uuid) + name = Column(String, nullable=False) + cost_gold = Column(Integer, nullable=False) + cards_per_open = Column(Integer, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + +class UserCard(Base): + __tablename__ = "user_cards" + user_id = Column(String, ForeignKey("users.id"), primary_key=True) + card_id = Column(String, ForeignKey("cards.id"), primary_key=True) + obtained_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="cards") + card = relationship("Card", back_populates="owners") + +class ChestOpen(Base): + __tablename__ = "chest_opens" + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + chest_id = Column(String, ForeignKey("chests.id"), nullable=False) + spent_gold = Column(Integer, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="chest_opens") + items = relationship("ChestOpenItem", back_populates="chest_open") + +class ChestOpenItem(Base): + __tablename__ = "chest_open_items" + id = Column(String, primary_key=True, default=generate_uuid) + chest_open_id = Column(String, ForeignKey("chest_opens.id"), nullable=False) + card_id = Column(String, ForeignKey("cards.id"), nullable=True) # Nullable if pure conversion without card entity reference logic? Actually SOP says card NON possessed -> NEW. If possessed -> CONVERTED. Usually still references the card. + rarity = Column(String, nullable=False) + outcome_type = Column(String, nullable=False) # NEW / CONVERTED + converted_gold = Column(Integer, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + chest_open = relationship("ChestOpen", back_populates="items") + +class IdempotencyKey(Base): + __tablename__ = "idempotency_keys" + key = Column(String, nullable=False) + user_id = Column(String, nullable=False) + endpoint = Column(String, nullable=False) + response_body = Column(String, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + __table_args__ = ( + UniqueConstraint('key', 'user_id', 'endpoint', name='uix_idempotency_key_user_endpoint'), + {"extend_existing": True} # Just in case + ) + # Note: Using composite primary key might be better but SOP says "UNIQUE (key, user_id, endpoint)". + # Let's add an ID or make the composite PK. SQLAlchemy needs a PK. + id = Column(Integer, primary_key=True, autoincrement=True)