mantimon-tcg/backend/project_plans/PHASE_1_DATABASE.json

526 lines
19 KiB
JSON

{
"meta": {
"version": "1.0.0",
"created": "2026-01-27",
"lastUpdated": "2026-01-27",
"planType": "phase",
"phaseId": "PHASE_1",
"phaseName": "Database + Infrastructure",
"description": "PostgreSQL models, Redis caching, CardService, and development environment setup",
"totalEstimatedHours": 40,
"totalTasks": 18,
"completedTasks": 18,
"masterPlan": "../PROJECT_PLAN_MASTER.json"
},
"goals": [
"Establish PostgreSQL as durable storage with async SQLAlchemy",
"Set up Redis for game state caching and future matchmaking",
"Implement GameStateManager with write-behind persistence",
"Create CardService to load bundled JSON into CardDefinitions",
"Configure environment-based settings (dev/staging/prod)",
"Docker compose for local development"
],
"architectureNotes": {
"gameStatePersistence": {
"strategy": "Write-behind cache",
"primary": "Redis (fast reads/writes during gameplay)",
"durable": "PostgreSQL (write at turn boundaries + game end)",
"recovery": "On restart, load active games from Postgres → Redis"
},
"asyncEverywhere": "All DB and Redis operations use async/await",
"sessionManagement": "Scoped async sessions with proper cleanup"
},
"directoryStructure": {
"db": "backend/app/db/",
"services": "backend/app/services/",
"config": "backend/app/config.py",
"docker": "backend/docker-compose.yml"
},
"tasks": [
{
"id": "DB-001",
"name": "Create environment config module",
"description": "Pydantic Settings class for environment-based configuration (database URLs, Redis URL, secrets)",
"category": "critical",
"priority": 1,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": [],
"files": [
{"path": "app/config.py", "status": "done"}
],
"details": [
"Use pydantic-settings for env var parsing",
"Support .env files for local dev",
"Separate configs: DATABASE_URL, REDIS_URL, SECRET_KEY, OAUTH_*",
"Environment detection: dev/staging/prod"
],
"estimatedHours": 2
},
{
"id": "DB-002",
"name": "Create Docker compose for local dev",
"description": "Docker compose file with PostgreSQL and Redis services",
"category": "critical",
"priority": 2,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-001"],
"files": [
{"path": "docker-compose.yml", "status": "done"},
{"path": ".env.example", "status": "done"}
],
"details": [
"PostgreSQL 15 with health check",
"Redis 7 with persistence disabled (dev only)",
"Volume mounts for data persistence",
"Network for service communication",
"Uses ports 5433 (Postgres) and 6380 (Redis) to avoid conflicts"
],
"estimatedHours": 1
},
{
"id": "DB-003",
"name": "Set up SQLAlchemy async engine",
"description": "Create async engine, session factory, and base model class",
"category": "critical",
"priority": 3,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-001"],
"files": [
{"path": "app/db/__init__.py", "status": "done"},
{"path": "app/db/session.py", "status": "done"},
{"path": "app/db/base.py", "status": "done"}
],
"details": [
"AsyncEngine with connection pooling",
"async_sessionmaker for scoped sessions",
"Base declarative class with common columns (id, created_at, updated_at)",
"get_session() async context manager"
],
"estimatedHours": 2
},
{
"id": "DB-004",
"name": "Create User model",
"description": "SQLAlchemy model for user accounts with OAuth support",
"category": "high",
"priority": 4,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-003"],
"files": [
{"path": "app/db/models/user.py", "status": "done"}
],
"details": [
"Fields: id (UUID), email, display_name, avatar_url",
"OAuth fields: oauth_provider, oauth_id (composite unique)",
"Premium fields: is_premium, premium_until (nullable datetime)",
"Timestamps: created_at, last_login",
"Index on (oauth_provider, oauth_id)"
],
"estimatedHours": 1.5
},
{
"id": "DB-005",
"name": "Create Collection model",
"description": "SQLAlchemy model for player card collections",
"category": "high",
"priority": 5,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-004"],
"files": [
{"path": "app/db/models/collection.py", "status": "done"}
],
"details": [
"Fields: id, user_id (FK), card_definition_id (str), quantity",
"Source tracking: source (enum: starter, booster, reward, purchase)",
"Timestamps: obtained_at",
"Unique constraint on (user_id, card_definition_id)",
"Index on user_id for fast collection lookups"
],
"estimatedHours": 1.5
},
{
"id": "DB-006",
"name": "Create Deck model",
"description": "SQLAlchemy model for player decks",
"category": "high",
"priority": 6,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-004"],
"files": [
{"path": "app/db/models/deck.py", "status": "done"}
],
"details": [
"Fields: id, user_id (FK), name, cards (JSONB), energy_cards (JSONB)",
"Validation: is_valid (bool), validation_errors (JSONB nullable)",
"Metadata: is_starter (bool), starter_type (nullable)",
"Timestamps: created_at, updated_at",
"Index on user_id"
],
"estimatedHours": 1.5
},
{
"id": "DB-007",
"name": "Create CampaignProgress model",
"description": "SQLAlchemy model for single-player campaign state",
"category": "high",
"priority": 7,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-004"],
"files": [
{"path": "app/db/models/campaign.py", "status": "done"}
],
"details": [
"Fields: id, user_id (FK unique), current_club, medals (JSONB)",
"Progress: defeated_npcs (JSONB), total_wins, total_losses",
"Economy: booster_packs (int), mantibucks (int)",
"Timestamps: created_at, updated_at"
],
"estimatedHours": 1.5
},
{
"id": "DB-008",
"name": "Create ActiveGame model",
"description": "SQLAlchemy model for in-progress games (Postgres backup)",
"category": "high",
"priority": 8,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-003"],
"files": [
{"path": "app/db/models/game.py", "status": "done"}
],
"details": [
"Fields: id (UUID), game_type (enum: campaign, freeplay, ranked)",
"Players: player1_id (FK), player2_id (FK nullable), npc_id (nullable)",
"State: rules_config (JSONB), game_state (JSONB), turn_number",
"Timing: started_at, last_action_at, turn_deadline (nullable)",
"Index on player IDs for reconnection lookup"
],
"estimatedHours": 2
},
{
"id": "DB-009",
"name": "Create GameHistory model",
"description": "SQLAlchemy model for completed games",
"category": "medium",
"priority": 9,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-003"],
"files": [
{"path": "app/db/models/game.py", "status": "done"}
],
"details": [
"Fields: id, game_type, player1_id, player2_id, npc_id",
"Result: winner_id, end_reason, turn_count, duration_seconds",
"Replay: replay_data (JSONB for future replay feature)",
"Timestamp: played_at",
"Indexes for leaderboard queries"
],
"estimatedHours": 1.5
},
{
"id": "DB-010",
"name": "Create models __init__ and export",
"description": "Consolidate all models in db/models/__init__.py",
"category": "medium",
"priority": 10,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-004", "DB-005", "DB-006", "DB-007", "DB-008", "DB-009"],
"files": [
{"path": "app/db/models/__init__.py", "status": "done"}
],
"details": [
"Import and re-export all models",
"Import Base for Alembic"
],
"estimatedHours": 0.5
},
{
"id": "DB-011",
"name": "Set up Alembic migrations",
"description": "Initialize Alembic and create initial migration",
"category": "high",
"priority": 11,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-010"],
"files": [
{"path": "alembic.ini", "status": "done"},
{"path": "app/db/migrations/env.py", "status": "done"},
{"path": "app/db/migrations/versions/7ac994d6f89c_initial_schema.py", "status": "done"},
{"path": "app/db/migrations/versions/ab8a0039fe55_allow_null_player1_id.py", "status": "done"}
],
"details": [
"Configure async Alembic",
"Auto-generate from SQLAlchemy models",
"Initial migration with all tables",
"Add to pyproject.toml dependencies",
"Black formatting enabled for migrations"
],
"estimatedHours": 2
},
{
"id": "DB-012",
"name": "Create Redis connection utilities",
"description": "Async Redis client with connection pooling",
"category": "high",
"priority": 12,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-001"],
"files": [
{"path": "app/db/redis.py", "status": "done"}
],
"details": [
"Use redis.asyncio (redis-py)",
"Connection pool configuration",
"get_redis() async context manager",
"Helper methods: get_json, set_json, delete"
],
"estimatedHours": 1.5
},
{
"id": "DB-013",
"name": "Create GameStateManager",
"description": "Redis-primary, Postgres-backup game state management",
"category": "critical",
"priority": 13,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-012", "DB-008"],
"files": [
{"path": "app/services/game_state_manager.py", "status": "done"}
],
"details": [
"load_state(): Try Redis, fallback to Postgres",
"save_to_cache(): Write to Redis only (fast path)",
"persist_to_db(): Write to Postgres (turn boundaries)",
"delete_game(): Clean up Redis + Postgres",
"recover_active_games(): Load from Postgres → Redis on startup",
"Key format: game:{game_id}"
],
"estimatedHours": 3
},
{
"id": "DB-014",
"name": "Create CardService",
"description": "Load card definitions from bundled JSON files",
"category": "critical",
"priority": 14,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": [],
"files": [
{"path": "app/services/card_service.py", "status": "done"},
{"path": "scripts/scrape_pokemon_pocket.py", "status": "done"},
{"path": "scripts/convert_cards.py", "status": "done"},
{"path": "data/definitions/", "status": "done"},
{"path": "data/raw/", "status": "done"}
],
"details": [
"Load from data/definitions/_index.json on startup",
"Parse JSON → CardDefinition models",
"get_card(card_id) → CardDefinition",
"get_cards_by_ids(card_ids) → dict[str, CardDefinition]",
"get_set_cards(set_code) → list[CardDefinition]",
"search(filters) for deck building UI",
"Includes scraper and converter scripts for card data pipeline",
"382 cards total (372 scraped + 10 basic energy)"
],
"estimatedHours": 3,
"notes": "Includes complete card data pipeline with scraper that properly extracts energy types from HTML"
},
{
"id": "DB-015",
"name": "Create database tests",
"description": "Tests for models, sessions, and basic CRUD",
"category": "high",
"priority": 15,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-011"],
"files": [
{"path": "tests/db/__init__.py", "status": "done"},
{"path": "tests/db/conftest.py", "status": "done"},
{"path": "tests/db/test_models.py", "status": "done"},
{"path": "tests/db/test_relationships.py", "status": "done"}
],
"details": [
"Uses real Postgres via testcontainers pattern (sync psycopg2 for fixtures)",
"Test each model: create, read, update, delete",
"Test relationships and constraints",
"Test JSONB serialization",
"Test cascade deletes and relationship integrity"
],
"estimatedHours": 4
},
{
"id": "DB-016",
"name": "Create GameStateManager tests",
"description": "Tests for Redis + Postgres state management",
"category": "high",
"priority": 16,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-013", "DB-015"],
"files": [
{"path": "tests/services/test_game_state_manager.py", "status": "done"},
{"path": "tests/services/conftest.py", "status": "done"}
],
"details": [
"Uses fakeredis for Redis mocking",
"Test cache hit (Redis has state)",
"Test cache miss (fallback to Postgres)",
"Test persist_to_db writes correctly",
"Test recovery on startup",
"Test cleanup on game end"
],
"estimatedHours": 3
},
{
"id": "DB-017",
"name": "Create CardService tests",
"description": "Tests for card loading and lookup",
"category": "high",
"priority": 17,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": ["DB-014"],
"files": [
{"path": "tests/services/test_card_service.py", "status": "done"},
{"path": "tests/scripts/test_convert_cards.py", "status": "done"}
],
"details": [
"Test loads all cards from JSON",
"Test get_card returns correct definition",
"Test get_cards_by_ids batch lookup",
"Test search with filters (type, set, rarity, stage, variant)",
"Test handles missing card gracefully",
"Test converter script validation and evolution chains"
],
"estimatedHours": 2
},
{
"id": "DB-018",
"name": "Update dependencies in pyproject.toml",
"description": "Add all required packages for Phase 1",
"category": "high",
"priority": 18,
"completed": true,
"completedDate": "2026-01-27",
"dependencies": [],
"files": [
{"path": "pyproject.toml", "status": "done"}
],
"details": [
"sqlalchemy[asyncio] >= 2.0",
"asyncpg (Postgres async driver)",
"alembic",
"redis >= 5.0 (async support)",
"pydantic-settings",
"psycopg2-binary (for test fixtures)",
"fakeredis[lua] (for Redis mocking in tests)"
],
"estimatedHours": 0.5
}
],
"testingStrategy": {
"approach": "Real Postgres via Docker + fakeredis for Redis",
"fixtures": "Async fixtures in conftest.py with sync psycopg2 for cleanup",
"coverage": "974 tests passing, high coverage on new code"
},
"weeklyRoadmap": {
"week1": {
"theme": "Foundation",
"tasks": ["DB-001", "DB-002", "DB-003", "DB-018"],
"goals": ["Dev environment running", "SQLAlchemy configured"],
"status": "complete"
},
"week2": {
"theme": "Models + Migrations",
"tasks": ["DB-004", "DB-005", "DB-006", "DB-007", "DB-008", "DB-009", "DB-010", "DB-011"],
"goals": ["All models defined", "Migrations working"],
"status": "complete"
},
"week3": {
"theme": "Services + Testing",
"tasks": ["DB-012", "DB-013", "DB-014", "DB-015", "DB-016", "DB-017"],
"goals": ["GameStateManager working", "CardService working", "Full test coverage"],
"status": "complete"
}
},
"acceptanceCriteria": [
{"criterion": "docker-compose up starts Postgres + Redis locally", "met": true},
{"criterion": "Alembic migrations run successfully", "met": true},
{"criterion": "All models have CRUD tests passing", "met": true},
{"criterion": "GameStateManager persists at turn boundaries", "met": true},
{"criterion": "GameStateManager recovers from Postgres on restart", "met": true},
{"criterion": "CardService loads all 382 cards from JSON", "met": true, "notes": "372 scraped + 10 basic energy"},
{"criterion": "All tests pass with testcontainers", "met": true, "notes": "974 tests passing"}
],
"completionSummary": {
"status": "COMPLETE",
"completedDate": "2026-01-27",
"totalTests": 974,
"keyDeliverables": [
"Full async SQLAlchemy infrastructure with 6 models",
"GameStateManager with Redis cache + Postgres persistence",
"CardService loading 382 card definitions",
"Complete card data pipeline (scraper + converter)",
"Comprehensive test suite with real database testing",
"FastAPI app with lifespan hooks for startup/shutdown"
],
"notableImplementationDetails": [
"Uses ports 5433/6380 to avoid conflicts with existing services",
"Scraper properly extracts energy types from HTML spans",
"pytest-asyncio fixtures use sync psycopg2 for reliable cleanup",
"fakeredis used for Redis mocking in service tests",
"FastAPI lifespan wires init_db, init_redis, CardService.load_all",
"get_session_dependency exported for FastAPI dependency injection",
"game_cache_ttl_seconds configurable via Settings",
"datetime.utcnow() replaced with datetime.now(UTC)"
],
"gapsFixedPostReview": [
"Added FastAPI lifespan hooks (startup/shutdown)",
"Exported get_session_dependency from app/db/__init__.py",
"Made game cache TTL configurable via Settings",
"Fixed datetime.utcnow() deprecation (4 occurrences)",
"Added /health/ready endpoint for readiness checks",
"Added CORS middleware"
]
},
"phase2Prerequisites": {
"description": "Items identified during Phase 1 review that should be addressed in Phase 2",
"items": [
{
"name": "Auth infrastructure (JWT)",
"description": "Add JWT encode/decode utilities using secret_key from Settings",
"priority": "high"
},
{
"name": "python-socketio integration",
"description": "Install python-socketio and create WebSocket handlers for real-time gameplay",
"priority": "high"
}
]
}
}