Add detailed Phase 3 (Collections + Decks) project plan

14 tasks covering card ownership, deck building, validation, and
starter deck selection. 29 estimated hours total.

Key components:
- CollectionService for card ownership CRUD
- DeckService with slot limits and validation
- DeckValidator for rule enforcement
- 5 starter deck definitions
- REST API endpoints for collections and decks
- ~80-90 tests planned

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-28 00:40:25 -06:00
parent f82bc8aa1f
commit 4859b2a9cb

View File

@ -0,0 +1,620 @@
{
"meta": {
"version": "1.0.0",
"created": "2026-01-28",
"lastUpdated": "2026-01-28",
"planType": "phase",
"phaseId": "PHASE_3",
"phaseName": "Collections + Decks",
"description": "Card ownership (collections), deck building, validation, and starter deck selection",
"totalEstimatedHours": 29,
"totalTasks": 14,
"completedTasks": 0,
"status": "not_started",
"masterPlan": "../PROJECT_PLAN_MASTER.json"
},
"goals": [
"Implement card ownership tracking (CollectionService) with quantity and source tracking",
"Create deck building service (DeckService) with CRUD operations",
"Implement comprehensive deck validation (card counts, ownership, Basic Pokemon requirement)",
"Enforce deck slot limits (5 free, unlimited premium)",
"Define 5 starter deck compositions using real card IDs",
"Create starter deck selection flow for new users",
"Build REST API endpoints for collections and decks"
],
"architectureNotes": {
"collectionModel": {
"existing": "Collection model in app/db/models/collection.py - already has user_id, card_definition_id, quantity, source (CardSource enum)",
"constraints": "Unique constraint on (user_id, card_definition_id) for upsert pattern",
"source_tracking": "CardSource enum: STARTER, BOOSTER, REWARD, PURCHASE, TRADE, GIFT"
},
"deckModel": {
"existing": "Deck model in app/db/models/deck.py - has JSONB cards/energy_cards fields",
"validation_state": "is_valid boolean and validation_errors JSONB for storing validation results",
"starter_flags": "is_starter and starter_type fields for starter deck tracking"
},
"deckRules": {
"mainDeck": "40 cards exactly (from DeckConfig)",
"energyDeck": "20 energy cards (Mantimon house rules - separate energy deck)",
"maxCopies": "Max 4 copies of any single card",
"basicPokemon": "Min 1 Basic Pokemon required",
"deckSlots": "5 for free users, 999 (unlimited) for premium (from User.max_decks property)"
},
"validationModes": {
"campaign": "Requires ownership - must own all cards in deck",
"freeplay": "Full collection unlocked - skip ownership validation"
},
"cardIdFormat": {
"pattern": "{set}-{number}-{name}",
"example": "a1-001-bulbasaur, a1-094-pikachu, a1-036-charizard-ex",
"source": "Loaded by CardService from data/definitions/ JSON files"
},
"starterDecks": {
"types": ["grass", "fire", "water", "psychic", "lightning"],
"classic": ["grass", "fire", "water"],
"rotating": ["psychic", "lightning"],
"composition": "40 Pokemon/Trainer cards + 20 energy (type-specific + colorless)"
}
},
"directoryStructure": {
"schemas": "backend/app/schemas/",
"services": "backend/app/services/",
"api": "backend/app/api/",
"data": "backend/app/data/",
"tests": "backend/tests/services/, backend/tests/api/"
},
"tasks": [
{
"id": "COLL-001",
"name": "Create Collection Pydantic schemas",
"description": "Define request/response models for collection operations",
"category": "critical",
"priority": 1,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{"path": "app/schemas/collection.py", "status": "create"}
],
"details": [
"CollectionEntryResponse: card_definition_id, quantity, source, obtained_at",
"CollectionResponse: total_unique_cards, total_card_count, entries list",
"CollectionAddRequest: card_definition_id, quantity (default 1), source",
"CollectionCardResponse: card_definition_id, quantity (for single card lookup)",
"Use existing CardSource enum from app.db.models.collection"
],
"estimatedHours": 1,
"notes": "Keep schemas focused on what API needs - model has more fields"
},
{
"id": "COLL-002",
"name": "Create Deck Pydantic schemas",
"description": "Define request/response models for deck operations",
"category": "critical",
"priority": 2,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{"path": "app/schemas/deck.py", "status": "create"}
],
"details": [
"DeckCreateRequest: name, cards (dict[str, int]), energy_cards (dict[str, int])",
"DeckUpdateRequest: name (optional), cards (optional), energy_cards (optional)",
"DeckResponse: id, name, cards, energy_cards, is_valid, validation_errors, is_starter, starter_type, created_at, updated_at",
"DeckListResponse: decks list, deck_count, deck_limit (None for premium = unlimited)",
"DeckValidateRequest: cards, energy_cards (validate without saving)",
"DeckValidationResponse: is_valid, errors list",
"StarterDeckSelectRequest: starter_type (one of grass/fire/water/psychic/lightning)"
],
"estimatedHours": 1.5,
"notes": "Deck response includes validation state from model"
},
{
"id": "COLL-003",
"name": "Create DeckValidator service",
"description": "Standalone validation logic for deck rules - no DB dependency",
"category": "critical",
"priority": 3,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{"path": "app/services/deck_validator.py", "status": "create"}
],
"details": [
"DeckValidationResult dataclass: is_valid (bool), errors (list[str])",
"DeckValidator class with validate_deck() method",
"Parameters: cards (dict[str, int]), energy_cards (dict[str, int]), owned_cards (dict[str, int] | None)",
"Validation rules:",
" 1. Total cards must equal 40 (from DeckConfig.min_size/max_size)",
" 2. Total energy must equal 20 (from DeckConfig.energy_deck_size)",
" 3. Max 4 copies of any card (from DeckConfig.max_copies_per_card)",
" 4. Min 1 Basic Pokemon (from DeckConfig.min_basic_pokemon)",
" 5. All card IDs must exist (via CardService.get_card)",
" 6. If owned_cards provided: must own enough copies of each card",
"Uses CardService singleton to validate card IDs and check Basic Pokemon",
"Returns all errors, not just first one (helpful for UI)"
],
"estimatedHours": 2,
"notes": "Keep validator separate from service for testability and reuse"
},
{
"id": "COLL-004",
"name": "Create CollectionService",
"description": "Service layer for card collection CRUD operations",
"category": "critical",
"priority": 4,
"completed": false,
"tested": false,
"dependencies": ["COLL-001"],
"files": [
{"path": "app/services/collection_service.py", "status": "create"}
],
"details": [
"CollectionService class following UserService pattern",
"get_collection(db, user_id) -> list[Collection]",
"get_card_quantity(db, user_id, card_definition_id) -> int",
"add_cards(db, user_id, card_definition_id, quantity, source) -> Collection",
" - Upsert pattern: increment quantity if exists, create if not",
" - Validate card_definition_id exists via CardService",
"remove_cards(db, user_id, card_definition_id, quantity) -> Collection | None",
" - Decrement quantity, delete row if quantity reaches 0",
"has_cards(db, user_id, card_requirements: dict[str, int]) -> bool",
" - Check if user owns at least the required quantity of each card",
"get_owned_cards_dict(db, user_id) -> dict[str, int]",
" - Returns {card_id: quantity} for deck validation",
"grant_starter_deck(db, user_id, starter_type) -> list[Collection]",
" - Add all cards from starter deck definition",
" - Use CardSource.STARTER for source",
"Global instance: collection_service = CollectionService()"
],
"estimatedHours": 2.5,
"notes": "Upsert pattern: SELECT FOR UPDATE + INSERT ON CONFLICT or merge"
},
{
"id": "COLL-005",
"name": "Create DeckService",
"description": "Service layer for deck CRUD with validation",
"category": "critical",
"priority": 5,
"completed": false,
"tested": false,
"dependencies": ["COLL-002", "COLL-003", "COLL-004"],
"files": [
{"path": "app/services/deck_service.py", "status": "create"}
],
"details": [
"DeckService class following service pattern",
"create_deck(db, user_id, name, cards, energy_cards, validate_ownership=True) -> Deck",
" - Check deck slot limit (user.max_decks)",
" - Run DeckValidator.validate_deck()",
" - If validate_ownership: pass owned_cards from CollectionService",
" - Store validation result in is_valid/validation_errors",
" - Allow saving invalid decks (with errors) - don't block UI",
"update_deck(db, user_id, deck_id, updates: DeckUpdateRequest) -> Deck | None",
" - Verify ownership (user_id matches)",
" - Re-validate after update",
"delete_deck(db, user_id, deck_id) -> bool",
" - Verify ownership before delete",
"get_deck(db, user_id, deck_id) -> Deck | None",
" - Return only if user owns it",
"get_user_decks(db, user_id) -> list[Deck]",
"can_create_deck(db, user: User) -> bool",
" - Check len(user.decks) < user.max_decks",
"get_deck_for_game(db, user_id, deck_id) -> list[CardDefinition]",
" - Expand deck dict to list of CardDefinition objects",
" - For use by GameEngine in future phases",
"validate_deck(cards, energy_cards, owned_cards=None) -> DeckValidationResult",
" - Wrapper around DeckValidator for API use",
"Global instance: deck_service = DeckService()"
],
"estimatedHours": 3,
"notes": "Allow invalid decks to be saved - validation errors shown in UI"
},
{
"id": "COLL-006",
"name": "Create starter deck definitions",
"description": "Define 5 starter decks with real card IDs from scraped data",
"category": "high",
"priority": 6,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{"path": "app/data/starter_decks.py", "status": "create"}
],
"details": [
"STARTER_DECKS dict mapping type to deck definition",
"Each deck: 40 cards + 20 energy",
"Structure: {name, cards: {card_id: qty}, energy_cards: {type: qty}}",
"",
"GRASS starter (using a1 grass cards):",
" - Bulbasaur line: a1-001-bulbasaur, a1-002-ivysaur, a1-003-venusaur",
" - Caterpie line: a1-005-caterpie, a1-006-metapod, a1-007-butterfree",
" - Bellsprout line: a1-018-bellsprout, a1-019-weepinbell, a1-020-victreebel",
" - Energy: grass (14), colorless (6)",
"",
"FIRE starter (using a1 fire cards):",
" - Charmander line: a1-033-charmander, a1-034-charmeleon, a1-035-charizard",
" - Growlithe line: a1-039-growlithe, a1-040-arcanine",
" - Ponyta line: a1-042-ponyta, a1-043-rapidash",
" - Energy: fire (14), colorless (6)",
"",
"WATER starter (using a1 water cards):",
" - Squirtle line: a1-053-squirtle, a1-054-wartortle, a1-055-blastoise",
" - Poliwag line: a1-059-poliwag, a1-060-poliwhirl, a1-061-poliwrath",
" - Horsea line: a1-070-horsea, a1-071-seadra",
" - Energy: water (14), colorless (6)",
"",
"PSYCHIC starter (using a1 psychic cards):",
" - Abra line, Gastly line, Drowzee line (IDs TBD from data)",
" - Energy: psychic (14), colorless (6)",
"",
"LIGHTNING starter (using a1 lightning cards):",
" - Pikachu line: a1-094-pikachu, a1-095-raichu",
" - Magnemite line: a1-097-magnemite, a1-098-magneton",
" - Voltorb line: a1-099-voltorb, a1-100-electrode",
" - Energy: lightning (14), colorless (6)",
"",
"get_starter_deck(starter_type) -> dict function",
"STARTER_TYPES = ['grass', 'fire', 'water', 'psychic', 'lightning']"
],
"estimatedHours": 2,
"notes": "Must use exact card IDs from data/definitions/ - verify with CardService"
},
{
"id": "COLL-007",
"name": "Create collections API router",
"description": "REST endpoints for collection management",
"category": "high",
"priority": 7,
"completed": false,
"tested": false,
"dependencies": ["COLL-004"],
"files": [
{"path": "app/api/collections.py", "status": "create"}
],
"details": [
"APIRouter with prefix='/collections', tags=['collections']",
"",
"GET /collections/me",
" - Auth: CurrentUser required",
" - Returns: CollectionResponse with all user's cards",
"",
"GET /collections/me/cards/{card_id}",
" - Auth: CurrentUser required",
" - Returns: CollectionCardResponse (card_id + quantity)",
" - 404 if card not in collection (quantity 0)",
"",
"POST /collections/admin/{user_id}/add",
" - Auth: Admin required (future - stub with flag)",
" - Body: CollectionAddRequest",
" - Returns: CollectionEntryResponse",
" - For testing/admin card grants",
"",
"All endpoints use DbSession and CurrentUser dependencies from deps.py"
],
"estimatedHours": 1.5,
"notes": "Admin endpoint useful for testing - can add admin role check later"
},
{
"id": "COLL-008",
"name": "Create decks API router",
"description": "REST endpoints for deck CRUD and validation",
"category": "high",
"priority": 8,
"completed": false,
"tested": false,
"dependencies": ["COLL-005"],
"files": [
{"path": "app/api/decks.py", "status": "create"}
],
"details": [
"APIRouter with prefix='/decks', tags=['decks']",
"",
"GET /decks",
" - Auth: CurrentUser required",
" - Returns: DeckListResponse (decks, count, limit)",
"",
"POST /decks",
" - Auth: CurrentUser required",
" - Body: DeckCreateRequest",
" - Returns: DeckResponse",
" - 400 if at deck limit",
"",
"GET /decks/{deck_id}",
" - Auth: CurrentUser required",
" - Returns: DeckResponse",
" - 404 if not found or not owned",
"",
"PUT /decks/{deck_id}",
" - Auth: CurrentUser required",
" - Body: DeckUpdateRequest",
" - Returns: DeckResponse",
" - 404 if not found or not owned",
"",
"DELETE /decks/{deck_id}",
" - Auth: CurrentUser required",
" - Returns: 204 No Content",
" - 404 if not found or not owned",
"",
"POST /decks/validate",
" - Auth: CurrentUser required",
" - Body: DeckValidateRequest",
" - Query param: ?mode=campaign|freeplay (default campaign)",
" - Returns: DeckValidationResponse",
" - For validating deck before saving"
],
"estimatedHours": 2.5,
"notes": "PUT is full replace for simplicity - PATCH could be added later"
},
{
"id": "COLL-009",
"name": "Add starter deck selection endpoint to users API",
"description": "Endpoint for new user starter deck selection flow",
"category": "high",
"priority": 9,
"completed": false,
"tested": false,
"dependencies": ["COLL-004", "COLL-005", "COLL-006"],
"files": [
{"path": "app/api/users.py", "status": "modify"}
],
"details": [
"POST /users/me/starter-deck",
" - Auth: CurrentUser required",
" - Body: StarterDeckSelectRequest {starter_type: str}",
" - Returns: DeckResponse for the created starter deck",
" - 400 if starter already selected",
" - 400 if invalid starter_type",
"",
"Logic:",
" 1. Check if user already has a starter deck (check collection for STARTER source)",
" 2. Validate starter_type is in STARTER_TYPES list",
" 3. Grant cards to collection via collection_service.grant_starter_deck()",
" 4. Create deck via deck_service.create_deck() with is_starter=True",
" 5. Return the created deck",
"",
"GET /users/me/starter-status",
" - Auth: CurrentUser required",
" - Returns: {has_starter: bool, starter_type: str | null}"
],
"estimatedHours": 1.5,
"notes": "Check for starter by source in collection OR is_starter flag on deck"
},
{
"id": "COLL-010",
"name": "Register API routers in main.py",
"description": "Mount collections and decks routers",
"category": "high",
"priority": 10,
"completed": false,
"tested": false,
"dependencies": ["COLL-007", "COLL-008"],
"files": [
{"path": "app/main.py", "status": "modify"}
],
"details": [
"Import collections and decks routers",
"app.include_router(collections.router, prefix='/api')",
"app.include_router(decks.router, prefix='/api')"
],
"estimatedHours": 0.5,
"notes": "Following same pattern as auth and users routers"
},
{
"id": "COLL-011",
"name": "Create DeckValidator tests",
"description": "Unit tests for deck validation logic",
"category": "high",
"priority": 11,
"completed": false,
"tested": false,
"dependencies": ["COLL-003"],
"files": [
{"path": "tests/services/test_deck_validator.py", "status": "create"}
],
"details": [
"Test valid deck passes all checks",
"Test deck with wrong card count fails (39, 41 cards)",
"Test deck with wrong energy count fails (19, 21 energy)",
"Test deck with 5+ copies of same card fails",
"Test deck with 0 Basic Pokemon fails",
"Test deck with invalid card ID fails",
"Test deck with insufficient owned cards fails (ownership mode)",
"Test deck validation skips ownership in freeplay mode",
"Test multiple errors returned together",
"Test validation uses CardService correctly",
"~15-20 tests"
],
"estimatedHours": 2,
"notes": "Mock CardService for unit tests"
},
{
"id": "COLL-012",
"name": "Create CollectionService tests",
"description": "Integration tests for collection operations",
"category": "high",
"priority": 12,
"completed": false,
"tested": false,
"dependencies": ["COLL-004"],
"files": [
{"path": "tests/services/test_collection_service.py", "status": "create"}
],
"details": [
"Test get_collection returns empty for new user",
"Test add_cards creates new entry",
"Test add_cards increments existing entry quantity",
"Test remove_cards decrements quantity",
"Test remove_cards deletes entry when quantity reaches 0",
"Test remove_cards returns None if card not owned",
"Test has_cards returns True when owned",
"Test has_cards returns False when insufficient",
"Test get_owned_cards_dict returns correct mapping",
"Test grant_starter_deck adds all cards with STARTER source",
"Test add_cards rejects invalid card_definition_id",
"~15-20 tests using real Postgres via testcontainers"
],
"estimatedHours": 2.5,
"notes": "Need CardService mock or loaded for card ID validation"
},
{
"id": "COLL-013",
"name": "Create DeckService tests",
"description": "Integration tests for deck operations",
"category": "high",
"priority": 13,
"completed": false,
"tested": false,
"dependencies": ["COLL-005"],
"files": [
{"path": "tests/services/test_deck_service.py", "status": "create"}
],
"details": [
"Test create_deck creates valid deck",
"Test create_deck stores validation errors for invalid deck",
"Test create_deck enforces deck slot limit for free user",
"Test create_deck allows unlimited decks for premium user",
"Test create_deck validates ownership when flag is True",
"Test create_deck skips ownership when flag is False",
"Test update_deck changes name only",
"Test update_deck changes cards and re-validates",
"Test update_deck rejects non-owned deck",
"Test delete_deck removes deck",
"Test delete_deck rejects non-owned deck",
"Test get_deck returns owned deck",
"Test get_deck returns None for non-owned deck",
"Test get_user_decks returns all user decks",
"Test can_create_deck respects limit",
"Test get_deck_for_game expands to CardDefinitions",
"~20-25 tests using real Postgres via testcontainers"
],
"estimatedHours": 3,
"notes": "Need fixtures for users with different premium status"
},
{
"id": "COLL-014",
"name": "Create API integration tests",
"description": "Integration tests for collection and deck endpoints",
"category": "high",
"priority": 14,
"completed": false,
"tested": false,
"dependencies": ["COLL-007", "COLL-008", "COLL-009"],
"files": [
{"path": "tests/api/test_collections_api.py", "status": "create"},
{"path": "tests/api/test_decks_api.py", "status": "create"}
],
"details": [
"Collections API tests:",
" - GET /collections/me returns empty for new user",
" - GET /collections/me returns cards after adding",
" - GET /collections/me/cards/{id} returns quantity",
" - GET /collections/me/cards/{id} returns 404 if not owned",
" - POST /collections/admin/{user_id}/add grants cards",
" - All endpoints require auth (401 without token)",
"",
"Decks API tests:",
" - GET /decks returns empty list for new user",
" - POST /decks creates deck",
" - POST /decks returns 400 at deck limit",
" - GET /decks/{id} returns deck",
" - GET /decks/{id} returns 404 for other user's deck",
" - PUT /decks/{id} updates deck",
" - DELETE /decks/{id} removes deck",
" - POST /decks/validate validates without saving",
"",
"Starter deck tests:",
" - POST /users/me/starter-deck grants cards and creates deck",
" - POST /users/me/starter-deck returns 400 if already selected",
" - GET /users/me/starter-status returns correct state",
"",
"~30-35 tests total"
],
"estimatedHours": 3.5,
"notes": "Use TestClient with dependency overrides like auth tests"
}
],
"testingStrategy": {
"approach": "Unit tests for validator, integration tests for services and API",
"mocking": "Mock CardService for unit tests, use loaded cards for integration",
"database": "Real Postgres via testcontainers for service/API tests",
"fixtures": "Create test users with different premium status, test card data",
"coverage": "Target ~80-90 new tests"
},
"acceptanceCriteria": [
{"criterion": "User can view their card collection", "met": false},
{"criterion": "User can create a deck with name, cards, and energy", "met": false},
{"criterion": "Deck validation enforces all rules (count, copies, Basic Pokemon)", "met": false},
{"criterion": "Deck validation reports all errors (not just first)", "met": false},
{"criterion": "Free users limited to 5 decks", "met": false},
{"criterion": "Premium users have unlimited decks", "met": false},
{"criterion": "New user can select a starter deck (one time)", "met": false},
{"criterion": "Starter deck selection grants cards and creates deck", "met": false},
{"criterion": "Campaign mode validates card ownership", "met": false},
{"criterion": "Freeplay mode skips ownership validation", "met": false},
{"criterion": "All endpoints require authentication", "met": false},
{"criterion": "All tests pass with high coverage", "met": false}
],
"securityConsiderations": [
"Deck ownership verified on all operations (no access to other users' decks)",
"Collection operations validate card IDs exist (no arbitrary string storage)",
"Admin endpoints should have role check (stubbed for now)",
"Deck slot limits enforced server-side (not just UI)",
"JSONB fields validated before storage"
],
"deferredItems": [
{
"item": "Card trading between users",
"reason": "Separate feature (FUTURE_TRADING in master plan)",
"priority": "future"
},
{
"item": "Deck import/export",
"reason": "Nice-to-have for sharing decks",
"priority": "low"
},
{
"item": "Deck statistics/analytics",
"reason": "Can be added to response later",
"priority": "low"
},
{
"item": "Admin role checking",
"reason": "Admin system not yet implemented",
"priority": "medium"
}
],
"dependencies": {
"existing": [
"Collection model (app/db/models/collection.py)",
"Deck model (app/db/models/deck.py)",
"CardService singleton (app/services/card_service.py)",
"User model with max_decks property",
"CurrentUser, DbSession dependencies (app/api/deps.py)"
],
"added": []
},
"phase2Prerequisites": {
"met": [
"User authentication (CurrentUser dependency)",
"User model with premium status",
"Collection model with source tracking",
"Deck model with JSONB cards storage",
"CardService for card lookup",
"DeckConfig for validation rules"
]
}
}