Files
akadmin bea91db64d Phase 3: Schema Layer - Pydantic v2 migration, deduplication, and missing schemas
- Migrated all schemas to Pydantic v2 syntax (model_config, ConfigDict)
- Fixed mutable default in ProtoMessageBase using Field(default_factory=datetime.now)
- Consolidated CardCollection and Wishlist schemas in user_card_collection.py
- Created game_schemas.py with GameCreate, GameResponse, GameJoinRequest, etc.
- Created mtg_card_schemas.py with MtgCardResponse, MtgCardSearchRequest, etc.
- Added CardImportBatchCreate, CardImportBatchResponse, UserCardImportCreate/Response schemas
- Fixed duplicate UserCardImportRecord class between card_import_batch.py and user_card_import_record.py
- Updated __init__.py with comprehensive schema exports
- Created verify_schemas.py for schema-model matching verification
2026-08-16 05:22:17 +00:00

1093 lines
43 KiB
Markdown

# MTG Online Backend - Code Structure Documentation
## Project Overview
**Location:** `/home/wall-o/projects/mtgonline/backend/`
**Stack:** FastAPI + SQLAlchemy (async) + Alembic + PostgreSQL + Redis
**Purpose:** MTG Online card database management, deck building, user management, and game infrastructure.
---
## 1. Models (`app/models/`)
### 1.1 Core Models (`models.py`)
**User** (`mtgonline_users`)
- `id`: Integer, primary key, indexed
- `username`: String(64), unique, indexed
- `password_hash`: String(128), bcrypt hash
- `salt`: String(128)
- `email`: String(255), indexed
- `country`: String(2)
- `real_name`: String(128)
- `avatar_bmp`: Text (Base64 encoded)
- `privlevel`: String(50), default "User"
- `is_active`: Boolean, default True
- `is_banned`: Boolean, default False
- `ban_reason`: Text
- `ban_ends`: DateTime
- `vip_status`: Integer (0=normal, 1=vip, 2=donator)
- `vip_expiry`: DateTime
- `creation_date`: DateTime, server default now()
- `last_login`: DateTime
- **Relationships:** `decklist_files`, `decklist_folders` (both cascade delete)
**MtonlineCard** (`mtgonline_cards`)
- `id`: Integer, primary key, indexed
- `source_id`: Integer, indexed (references mtg_cards.id in mtgdata)
- `name`: String(255), indexed
- `mana_cost`: String(255)
- `type_line`: String(255)
- `oracle_text`: Text
- `power`: String(50)
- `toughness`: String(50)
- `rarity`: String(50)
- `layout`: String(50)
- `artist`: String(255)
- `flavor_text`: Text
- `numbers`: String(100)
- `identifiers`: Text (JSON string)
- `images`: Text (JSON string)
- `image`: Text (card image URL)
- `set_code`: String(10), indexed
- `set_name`: String(255)
- `card_parts`: Text (comma-separated face names)
- `keywords`: Text (comma-separated)
- `legalities`: Text (JSON of format legality)
- `synced_at`: DateTime, onupdate now()
- `created_at`: DateTime, server default now()
**DecklistFolder** (`mtgonline_decklist_folders`)
- `id`: Integer, primary key, indexed
- `owner_id`: Integer, FK to mtgonline_users.id
- `name`: String(255)
- `parent_id`: Integer, FK to self (recursive)
- `creation_date`: DateTime
- **Relationships:** `owner`, `children` (recursive), `parent`, `files` (cascade delete)
**DecklistFile** (`mtgonline_decklist_files`)
- `id`: Integer, primary key, indexed
- `folder_id`: Integer, FK to mtgonline_decklist_folders.id
- `owner_id`: Integer, FK to mtgonline_users.id
- `name`: String(255)
- `content`: Text (native XML or plain text)
- `format`: String(50), default "native"
- `status`: String(20), default "DRAUGHT"
- `creation_date`: DateTime
- **Relationships:** `folder`, `owner`, `card_links` (added in mirror_models.py)
**Room** (`mtgonline_rooms`)
- `id`: Integer, primary key, indexed
- `name`: String(100), unique
- `description`: Text
- `is_password_protected`: Boolean
- `password_hash`: String(128)
- `creation_date`: DateTime
- **Relationships:** `game_types` (cascade delete)
**RoomGameType** (`mtgonline_rooms_gametypes`)
- `id`: Integer, primary key, indexed
- `room_id`: Integer, FK to mtgonline_rooms.id
- `name`: String(100)
- `description`: Text
- **Relationships:** `room`
**Ban** (`mtgonline_bans`)
- `id`: Integer, primary key, indexed
- `user_id`: Integer, FK to mtgonline_users.id
- `server_id`: Integer
- `reason`: Text
- `moderators`: String(255)
- `ip_address`: String(45)
- `expiration_time`: DateTime
- `active`: Boolean, default True
- `creation_date`: DateTime
- **Relationships:** `user`
**GameLog** (`mtgonline_log`)
- `id`: Integer, primary key, indexed
- `room_id`: Integer, FK to mtgonline_rooms.id
- `player_id`: Integer, FK to mtgonline_users.id
- `message`: Text
- `timestamp`: DateTime
- **Relationships:** `room`, `player`
**AuditLog** (`mtgonline_audit`)
- `id`: Integer, primary key, indexed
- `admin_id`: Integer, FK to mtgonline_users.id
- `action_type`: String(50)
- `target_user_id`: Integer, FK to mtgonline_users.id
- `details`: Text
- `ip_address`: String(45)
- `timestamp`: DateTime
- **Relationships:** `admin`, `target_user`
**Indexes:**
- `idx_decks_owner` on DecklistFile.owner_id
- `idx_decks_folder` on DecklistFile.folder_id
- `idx_bans_active` on Ban.active
- `idx_log_timestamp` on GameLog.timestamp
---
### 1.2 MTG Models (`mtg_models.py`)
**MtgSet** (`mtg_sets`)
- `id`: Integer, primary key, indexed
- `code`: String(10), unique, indexed
- `name`: String(255)
- `type`: String(100)
- `release_date`: DateTime
- `base_set_size`: Integer
- `total_size`: Integer
- `is_foil_only`: Integer
- `is_non_foil_only`: Integer
- `digital`: Integer
- `icon_svg_url`: Text
- `parent_code`: String(10)
- `mtgo_code`: String(10)
- `image`: Text
- `updated_at`: DateTime
- **Relationships:** `cards`
**MtgCard** (`mtg_cards`)
- `id`: Integer, primary key, indexed
- `set_id`: Integer, FK to mtg_sets.id, indexed
- `name`: String(255), indexed
- `mana_cost`: String(255), indexed
- `type_line`: String(255), indexed
- `oracle_text`: Text
- `power`: String(50)
- `toughness`: String(50)
- `rarity`: String(50), indexed
- `layout`: String(50)
- `artist`: String(255)
- `flavor_text`: Text
- `numbers`: String(100)
- `identifiers`: Text (JSON)
- `images`: Text (JSON)
- `image`: Text
- `updated_at`: DateTime
- **Relationships:** `set`
**Indexes:**
- `idx_mtg_cards_name_set` on MtgCard.name, MtgCard.set_id
- `idx_mtg_cards_type` on MtgCard.type_line
- `idx_mtg_cards_rarity` on MtgCard.rarity
---
### 1.3 Mirror Models (`mirror_models.py`)
**MtgCardMirror** (`mtg_cards_mirror`)
- `id`: Integer, primary key, indexed
- `source_id`: Integer, indexed (references mtg_cards.id)
- `name`: String(255), indexed
- `mana_cost`: String(255)
- `type_line`: String(255)
- `oracle_text`: Text
- `power`: String(50)
- `toughness`: String(50)
- `rarity`: String(50)
- `layout`: String(50)
- `artist`: String(255)
- `flavor_text`: Text
- `numbers`: String(100)
- `identifiers`: Text (JSON)
- `images`: Text (JSON)
- `image`: Text
- `card_parts`: Text
- `keywords`: Text
- `legalities`: Text (JSON)
- `set_code`: String(10), indexed
- `set_name`: String(255)
- `synced_at`: DateTime, onupdate now()
- `created_at`: DateTime
- **Relationships:** `deck_links` (cascade delete)
**DeckCardLink** (`deck_card_links`)
- `id`: Integer, primary key, indexed
- `deck_id`: Integer, FK to mtgonline_decklist_files.id, CASCADE delete
- `card_id`: Integer, FK to mtg_cards_mirror.id
- `quantity`: Integer, default 1
- `zone`: String(20), default "main"
- **Constraints:** UniqueConstraint(deck_id, card_id, zone)
- **Indexes:** `idx_deck_card_deck`, `idx_deck_card_card`
- **Relationships:** `deck`, `card`
**Note:** DecklistFile has a back-reference added: `card_links` relationship to DeckCardLink
---
### 1.4 User Data Models (`user_data.py`)
**UserSession** (`user_sessions`)
- `id`: BigInteger, primary key
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
- `session_token_hash`: String(255), unique, indexed
- `ip_address`: String(45)
- `user_agent`: Text
- `created_at`: DateTime
- `expires_at`: DateTime
- `is_active`: Boolean, default True
- **Relationships:** `user`
**DeckVersion** (`deck_versions`)
- `id`: BigInteger, primary key
- `deck_id`: Integer, FK to mtgonline_decklist_files.id, CASCADE, indexed
- `version_number`: Integer
- `content`: Text
- `status`: String(20), default "DRAFT"
- `comment`: Text
- `created_at`: DateTime
- **Relationships:** `deck`
**GameReplay** (`game_replays`)
- `id`: BigInteger, primary key
- `game_uuid`: String(36), unique
- `room_id`: Integer, FK to mtgonline_rooms.id, indexed
- `game_type`: String(50)
- `format`: String(50)
- `duration_seconds`: Integer
- `start_time`: DateTime
- `end_time`: DateTime
- `status`: String(20), default "IN_PROGRESS"
- `replay_data`: JSON
- `created_at`: DateTime
- `updated_at`: DateTime
- **Relationships:** `players` (cascade), `outcomes` (cascade)
**ReplayPlayer** (`replay_players`)
- `id`: BigInteger, primary key
- `replay_id`: BigInteger, FK to game_replays.id, CASCADE, indexed
- `user_id`: Integer, FK to mtgonline_users.id, indexed
- `position`: Integer
- `deck_id`: Integer, FK to mtgonline_decklist_files.id
- `won`: Boolean
- `lost`: Boolean
- `concession`: Boolean, default False
- `turn_one`: Boolean, default False
- `created_at`: DateTime
- **Relationships:** `replay`, `user`, `deck`
**GameOutcome** (`game_outcomes`)
- `id`: BigInteger, primary key
- `user_id`: Integer, FK to mtgonline_users.id, indexed
- `game_uuid`: String(36), FK to game_replays.game_uuid, indexed
- `outcome`: String(20), indexed
- `opponent_id`: Integer, FK to mtgonline_users.id
- `format`: String(50)
- `rating_before`: Integer
- `rating_after`: Integer
- `rating_change`: Integer
- `created_at`: DateTime
- **Relationships:** `replay`, `user`, `opponent`
**UserStatistics** (`user_statistics`)
- `user_id`: Integer, FK to mtgonline_users.id, primary key
- `total_games`: Integer, default 0
- `total_wins`: Integer, default 0
- `total_losses`: Integer, default 0
- `total_concessions`: Integer, default 0
- `win_rate`: Float, default 0.0
- `current_streak`: Integer, default 0
- `best_streak`: Integer, default 0
- `average_rating`: Float, default 0.0
- `last_game_date`: DateTime
- `updated_at`: DateTime
- **Relationships:** `user`
**UserCardCollection** (`user_card_collection`)
- `id`: BigInteger, primary key
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
- `card_id`: Integer, indexed
- `quantity`: Integer, default 1
- `condition`: String(20), default "NEAR_MINT"
- `language`: String(5), default "EN"
- `is_foil`: Boolean, default False
- `is_alt_art`: Boolean, default False
- `acquired_date`: DateTime
- `acquisition_method`: String(50)
- `notes`: Text
- `created_at`: DateTime
- `updated_at`: DateTime
- **Constraints:** UniqueConstraint(user_id, card_id, is_foil, is_alt_art)
- **Indexes:** `idx_collection_user_card`
- **Relationships:** `user`
**CardWishlist** (`card_wishlist`)
- `id`: BigInteger, primary key
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE
- `card_id`: Integer
- `max_price`: Float
- `notes`: Text
- `created_at`: DateTime
- **Constraints:** UniqueConstraint(user_id, card_id)
- **Relationships:** `user`
**UserGroup** (`user_groups`)
- `id`: BigInteger, primary key
- `name`: String(100)
- `description`: Text
- `owner_id`: Integer, FK to mtgonline_users.id, indexed
- `is_public`: Boolean, default True
- `max_members`: Integer, default 50
- `created_at`: DateTime
- `updated_at`: DateTime
- **Relationships:** `owner`, `members` (cascade), `messages` (cascade)
**GroupMember** (`group_members`)
- `id`: BigInteger, primary key
- `group_id`: BigInteger, FK to user_groups.id, CASCADE, indexed
- `user_id`: Integer, FK to mtgonline_users.id, indexed
- `role`: String(20), default "MEMBER"
- `joined_at`: DateTime
- **Constraints:** UniqueConstraint(group_id, user_id)
- **Relationships:** `group`, `user`
**GroupChatMessage** (`group_chat_messages`)
- `id`: BigInteger, primary key
- `group_id`: BigInteger, FK to user_groups.id, CASCADE, indexed
- `sender_id`: Integer, FK to mtgonline_users.id, indexed
- `message`: Text
- `created_at`: DateTime, indexed
- **Relationships:** `group`, `sender`
**UserNetwork** (`user_networks`)
- `id`: BigInteger, primary key
- `name`: String(100)
- `description`: Text
- `creator_id`: Integer, FK to mtgonline_users.id
- `is_public`: Boolean, default True
- `created_at`: DateTime
- **Relationships:** `creator`, `members` (cascade)
**NetworkMember** (`network_members`)
- `id`: BigInteger, primary key
- `network_id`: BigInteger, FK to user_networks.id, CASCADE, indexed
- `user_id`: Integer, FK to mtgonline_users.id, indexed
- `role`: String(20), default "MEMBER"
- `joined_at`: DateTime
- **Constraints:** UniqueConstraint(network_id, user_id)
- **Relationships:** `network`, `user`
**UserPreference** (`user_preferences`)
- `user_id`: Integer, FK to mtgonline_users.id, primary key
- `theme`: String(20), default "light"
- `notifications_enabled`: Boolean, default True
- `email_notifications`: Boolean, default True
- `auto_save_decks`: Boolean, default True
- `default_format`: String(50), default "standard"
- `language`: String(5), default "EN"
- `updated_at`: DateTime
- **Relationships:** `user`
**UserActivityLog** (`user_activity_log`)
- `id`: BigInteger, primary key
- `user_id`: Integer, FK to mtgonline_users.id, indexed
- `activity_type`: String(50), indexed
- `activity_data`: JSON
- `ip_address`: String(45)
- `created_at`: DateTime, indexed
- **Relationships:** `user`
---
### 1.5 User Deck Models (`user_deck.py`)
**UserDeck** (`user_decks`)
- `id`: BigInteger, primary key, autoincrement
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
- `name`: String(255), indexed
- `status`: String(20), default "DRAFT", indexed
- `folder_id`: Integer, FK to mtgonline_decklist_folders.id
- `format`: String(50), default "standard"
- `notes`: Text
- `is_precedent`: Boolean, default False, indexed
- `precedent_name`: String(255)
- `created_at`: DateTime
- `updated_at`: DateTime
- **Relationships:** `user`, `folder`, `cards` (cascade, ordered by id)
**UserDeckCard** (`user_deck_cards`)
- `id`: BigInteger, primary key, autoincrement
- `deck_id`: BigInteger, FK to user_decks.id, CASCADE, indexed
- `card_id`: Integer, FK to mtgonline_cards.id, indexed
- `quantity`: Integer, default 1
- `zone`: String(20), default "main"
- `position`: Integer
- **Constraints:** UniqueConstraint(deck_id, card_id, zone)
- **Indexes:** `idx_deck_cards_deck`, `idx_deck_cards_card`
- **Relationships:** `deck`, `card`
**DeckPrecedent** (`deck_precedents`)
- `id`: BigInteger, primary key, autoincrement
- `name`: String(255), indexed
- `description`: Text
- `format`: String(50), default "standard"
- `is_public`: Boolean, default True, indexed
- `created_by`: Integer, FK to mtgonline_users.id
- `created_at`: DateTime
- `updated_at`: DateTime
- **Relationships:** `creator`, `cards` (cascade)
**DeckPrecedentCard** (`deck_precedent_cards`)
- `id`: BigInteger, primary key, autoincrement
- `precedent_id`: BigInteger, FK to deck_precedents.id, CASCADE, indexed
- `card_id`: Integer, indexed
- `quantity`: Integer, default 1
- `zone`: String(20), default "main"
- **Constraints:** UniqueConstraint(precedent_id, card_id, zone)
- **Relationships:** `precedent`
**CardSuggestion** (`card_suggestions`)
- `id`: BigInteger, primary key, autoincrement
- `deck_id`: BigInteger, FK to user_decks.id, CASCADE, indexed
- `card_id`: Integer, indexed
- `source_card_id`: Integer
- `suggestion_type`: String(50), default "SIMILAR"
- `confidence`: Float
- `notes`: Text
- `created_at`: DateTime
- **Constraints:** UniqueConstraint(deck_id, card_id, source_card_id)
- **Relationships:** `deck`
---
### 1.6 Card Import Models
**CardImportBatch** (`card_import_batches`)
- `id`: Integer, primary key, autoincrement
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
- `filename`: String(255)
- `file_type`: String(10) (xlsx, csv, json, ods)
- `file_size`: Integer (bytes)
- `status`: String(20), default "pending", indexed (pending, processing, completed, failed)
- `total_cards`: Integer, default 0
- `matched_cards`: Integer, default 0
- `unmatched_cards`: Integer, default 0
- `match_results`: JSON
- `error_message`: Text
- `created_at`: DateTime
- `updated_at`: DateTime
- **Relationships:** `user`
**UserCardImportRecord** (`user_card_imports_confirmed`)
- `id`: Integer, primary key, autoincrement
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
- `batch_id`: Integer, FK to card_import_batches.id, CASCADE, indexed
- `is_confirmed`: Boolean, default True
- `confirmed_at`: DateTime
- **Relationships:** `user`, `batch`
---
## 2. Schemas (`app/schemas/`)
### 2.1 Core Schemas (`schemas.py`)
**Authentication Schemas:**
- `LoginRequest`: username (3-64 chars), password (6-128 chars)
- `LoginResponse`: access_token, refresh_token, token_type="bearer", user (dict)
- `RefreshTokenRequest`: refresh_token
- `TokenResponse`: access_token, token_type="bearer"
**User Schemas:**
- `UserBase`: username, email (optional), country (optional, max 2), real_name (optional)
- `UserCreate`: extends UserBase, password (min 8 chars)
- `UserUpdate`: email, country, real_name, new_password (optional, min 8, max 128)
- `UserResponse`: id, username, email, country, real_name, privlevel, vip_status, is_active, is_banned, ban_reason, creation_date, last_login (from_attributes)
**Deck Schemas:**
- `DeckCreate`: name (1-255 chars), content (min 1), folder_id (optional), format="native" (pattern: native|plain), status="DRAUGHT" (pattern: DRAUGHT|FINAL)
- `DeckUpdate`: name, content, folder_id, status (all optional)
- `DeckResponse`: id, name, content, format, status, folder_id, owner_id, creation_date (from_attributes)
- `FolderCreate`: name (1-255 chars), parent_id (optional)
- `FolderResponse`: id, name, parent_id, owner_id, creation_date (from_attributes)
**Game Schemas:**
- `GameCreate`: room_id, game_type (optional), description (optional), password (optional)
- `GameResponse`: id, room_id, game_type, description, with_password, max_players, player_count, started, creation_date (from_attributes)
**Room Schemas:**
- `RoomResponse`: id, name, description, is_password_protected, game_types (List[str]), player_count, creation_date (from_attributes)
**Ban Schemas:**
- `BanCreate`: user_id, reason (min 1, max 1000), expiration_time (optional)
- `BanResponse`: id, user_id, reason, moderators, expiration_time, active, creation_date (from_attributes)
**Error Schemas:**
- `ErrorResponse`: detail (str)
- `ValidationErrorResponse`: detail (List[dict])
**Pagination Schemas:**
- `PaginationParams`: page (1+, default 1), page_size (1-100, default 50)
- `PaginatedResponse`: items (List[dict]), total, page, page_size, total_pages
**Card Mirror Schemas:**
- `CardMirrorResponse`: id, source_id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, artist, flavor_text, numbers, identifiers, images, image, card_parts, keywords, legalities, set_code, set_name, synced_at, created_at (from_attributes)
- `DeckCardLinkResponse`: id, deck_id, card_id, quantity, zone, card (CardMirrorResponse) (from_attributes)
- `DeckWithCardsResponse`: id, name, content, format, status, folder_id, owner_id, creation_date, card_links (List[DeckCardLinkResponse]) (from_attributes)
---
### 2.2 User Data Schemas (`user_data_schemas.py`)
**Enums:**
- `DeckVersionStatus`: DRAFT, FINAL, ARCHIVED
- `GameReplayStatus`: IN_PROGRESS, COMPLETED, FAILED, CANCELLED
- `GameOutcomeType`: WIN, LOSS, CONCESSION, DISCONNECT
- `GroupMemberRole`: OWNER, ADMIN, MEMBER
- `NetworkMemberRole`: OWNER, ADMIN, MEMBER
- `UserPreferenceTheme`: LIGHT, DARK, SYSTEM
- `ActivityType`: LOGIN, LOGOUT, DECK_EDIT, GAME_PLAYED, CARD_ACQUIRED, CARD_TRADED, GROUP_CREATED, GROUP_JOINED
**Session Schemas:**
- `SessionResponse`: id, user_id, ip_address, user_agent, created_at, expires_at, is_active (from_attributes)
- `SessionCleanupResponse`: cleaned_count, message
**Deck Version Schemas:**
- `DeckVersionCreate`: content (min 1), status=DeckVersionStatus.DRAFT, comment (optional)
- `DeckVersionUpdate`: content, status, comment (all optional)
- `DeckVersionResponse`: id, deck_id, version_number, content, status, comment, created_at (from_attributes)
- `DeckVersionListResponse`: versions (List[DeckVersionResponse]), total
**Game Replay Schemas:**
- `GameReplayCreate`: game_uuid (36 chars), room_id, game_type, format, duration_seconds, start_time, end_time, status=IN_PROGRESS, replay_data (Dict)
- `GameReplayUpdate`: room_id, game_type, format, duration_seconds, end_time, status, replay_data (all optional)
- `GameReplayResponse`: id, game_uuid, room_id, game_type, format, duration_seconds, start_time, end_time, status, replay_data, created_at, updated_at, players (List[Dict]) (from_attributes)
- `GameReplayListResponse`: replays (List[GameReplayResponse]), total, page, page_size, total_pages
**Game Outcome Schemas:**
- `GameOutcomeCreate`: game_uuid, outcome=GameOutcomeType, opponent_id, format, rating_before, rating_after, rating_change
- `GameOutcomeResponse`: id, user_id, game_uuid, outcome, opponent_id, format, rating_before, rating_after, rating_change, created_at (from_attributes)
- `GameOutcomeListResponse`: outcomes (List[GameOutcomeResponse]), total
**User Statistics Schemas:**
- `UserStatisticsResponse`: user_id, total_games, total_wins, total_losses, total_concessions, win_rate, current_streak, best_streak, average_rating, last_game_date, updated_at (from_attributes)
- `StatisticsUpdateResponse`: user_id, total_games, total_wins, total_losses, win_rate, current_streak, updated_at
**Card Collection Schemas:**
- `CardCollectionCreate`: card_id, quantity=1 (ge=1), condition="NEAR_MINT" (max 20), language="EN" (max 5), is_foil=False, is_alt_art=False, acquired_date, acquisition_method, notes
- `CardCollectionUpdate`: quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes (all optional)
- `CardCollectionResponse`: id, user_id, card_id, quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes, created_at, updated_at (from_attributes)
- `CardCollectionListResponse`: cards (List[CardCollectionResponse]), total, page, page_size, total_pages
**Wishlist Schemas:**
- `WishlistCreate`: card_id, max_price, notes
- `WishlistUpdate`: max_price, notes (optional)
- `WishlistResponse`: id, user_id, card_id, max_price, notes, created_at (from_attributes)
- `WishlistListResponse`: items (List[WishlistResponse]), total
**Group Schemas:**
- `GroupCreate`: name (1-100 chars), description, is_public=True, max_members=50 (ge=2, le=500)
- `GroupUpdate`: name, description, is_public, max_members (optional)
- `GroupMemberCreate`: user_id, role=GroupMemberRole.MEMBER
- `GroupMemberUpdate`: role=GroupMemberRole
- `GroupMemberRemove`: user_id
- `GroupResponse`: id, name, description, owner_id, is_public, max_members, created_at, updated_at, member_count=0, is_member=False (from_attributes)
- `GroupListResponse`: groups (List[GroupResponse]), total
- `GroupChatMessageCreate`: message (1-2000 chars)
- `GroupChatMessageResponse`: id, group_id, sender_id, sender_username, message, created_at (from_attributes)
- `GroupChatMessageListResponse`: messages (List[GroupChatMessageResponse]), total, page, page_size, total_pages
**Network Schemas:**
- `NetworkCreate`: name (1-100 chars), description, is_public=True
- `NetworkUpdate`: name, description, is_public (optional)
- `NetworkMemberCreate`: user_id, role=NetworkMemberRole.MEMBER
- `NetworkResponse`: id, name, description, creator_id, is_public, created_at, member_count=0, is_member=False (from_attributes)
- `NetworkListResponse`: networks (List[NetworkResponse]), total
**Preference Schemas:**
- `UserPreferenceUpdate`: theme, notifications_enabled, email_notifications, auto_save_decks, default_format, language (all optional)
- `UserPreferenceResponse`: user_id, theme, notifications_enabled, email_notifications, auto_save_decks, default_format, language, updated_at (from_attributes)
**Activity Log Schemas:**
- `ActivityLogEntry`: id, user_id, activity_type, activity_data, ip_address, created_at (from_attributes)
- `ActivityLogListResponse`: entries (List[ActivityLogEntry]), total, page, page_size, total_pages
**Generic Schemas:**
- `MessageResponse`: message (str)
- `CountResponse`: count (int)
- `ErrorDetail`: error (str), detail (str)
---
### 2.3 User Deck Schemas (`user_deck_schemas.py`)
**Enums:**
- `DeckStatus`: DRAFT, FINAL
- `DeckZone`: MAIN, SIDEBOARD
- `SuggestionType`: SIMILAR, PAIRING, ALTERNATIVE
**Deck Schemas:**
- `UserDeckCreate`: name (1-255 chars), folder_id, format="standard" (max 50), notes, is_precedent=False, precedent_name
- `UserDeckUpdate`: name, folder_id, format, notes, status, is_precedent, precedent_name (all optional)
- `UserDeckResponse`: id, user_id, name, status, folder_id, format, notes, is_precedent, precedent_name, created_at, updated_at, card_count=0, is_owner=False (from_attributes)
- `UserDeckListResponse`: decks (List[UserDeckResponse]), total, page, page_size, total_pages
**Deck Card Schemas:**
- `DeckCardCreate`: card_id, quantity=1 (ge=1), zone=DeckZone.MAIN, position
- `DeckCardUpdate`: quantity, zone, position (optional)
- `DeckCardResponse`: id, deck_id, card_id, quantity, zone, position, created_at (from_attributes)
- `DeckCardWithDetailsResponse`: extends DeckCardResponse, card_name="", card_type_line="", card_image
- `DeckCardListResponse`: cards (List[DeckCardWithDetailsResponse]), total
**Deck Precedent Schemas:**
- `PrecedentCreate`: name (1-255 chars), description, format="standard" (max 50), is_public=True
- `PrecedentUpdate`: name, description, format, is_public (optional)
- `PrecedentResponse`: id, name, description, format, is_public, created_by, created_at, updated_at, card_count=0 (from_attributes)
- `PrecedentListResponse`: precedents (List[PrecedentResponse]), total, page, page_size, total_pages
**Card Suggestion Schemas:**
- `SuggestionCreate`: card_id, source_card_id, suggestion_type=SuggestionType.SIMILAR, confidence (0.0-1.0), notes
- `SuggestionResponse`: id, deck_id, card_id, source_card_id, suggestion_type, confidence, notes, created_at, card_name="" (from_attributes)
- `SuggestionListResponse`: suggestions (List[SuggestionResponse]), total
**Deck Action Schemas:**
- `DeckFinalizeRequest`: status=DeckStatus.FINAL
- `DeckFinalizeResponse`: deck_id, status, message
- `DeckDeleteResponse`: deck_id, message
**Search Schemas:**
- `CardSearchRequest`: query (1-100 chars), limit=50 (1-200), offset=0 (ge=0)
- `CardSearchResponse`: cards (List[Dict]), total, page, page_size, total_pages
**Generic Schemas:**
- `MessageResponse`: message (str)
- `CountResponse`: count (int)
---
### 2.4 Card Import Schemas (`card_import_schemas.py`)
- `CardImportRequest`: card_names (List[str], 1-10000 items)
- `CardImportResponse`: message, card_count, card_names, imported_at
- `CardImportStatusResponse`: has_import, card_count, card_names, last_imported
- `CardMatchResult`: card_id, card_name, matched_name, match_type (exact|fuzzy|partial), confidence (0.0-1.0)
- `CardImportSummary`: total_cards, matched_cards (List[CardMatchResult]), unmatched_cards (List[str]), import_id
- `MessageResponse`: message (str)
- `CountResponse`: count (int)
- `ErrorResponse`: detail (str), error_code
---
### 2.5 Card Search Schemas (`card_search_schemas.py`)
- `CardResponse`: id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, colors, set_code, set_name, identifiers (Dict), images (Dict) (from_attributes)
- `SetResponse`: id, name, code, release_date, card_count (from_attributes)
- `CardTypeResponse`: type (str)
- `CardSearchResponse`: cards (List[Dict]), total, page, page_size, total_pages
- `CardImportResponse`: message, batch_id, card_count, status, imported_at
- `CardImportStatusResponse`: has_import, batch_id, status, card_count, matched_count, unmatched_count, last_imported, error_message
- `CardMatchResult`: card_id, card_name, matched_name, match_type (exact|high_confidence|low_confidence), confidence (0.0-1.0)
- `CardImportSummary`: total_cards, matched_cards (List[CardMatchResult]), unmatched_cards (List[str]), import_id
- `MessageResponse`: message (str)
- `CountResponse`: count (int)
- `ErrorResponse`: detail (str), error_code
---
### 2.6 Protocol Schemas (`proto_messages.py`)
**Base:**
- `ProtoMessageBase`: message_type, timestamp
**Commands:**
- `SessionCommand`: message_type="SessionCommand", cmd_type (int), cmd_id=0, data (Dict)
- `GameCommand`: message_type="GameCommand", cmd_type (int), cmd_id=0, player_id, data (Dict)
- `GameEvent`: message_type="GameEvent", event_type (int), player_id, data (Dict)
- `Response`: message_type="Response", cmd_id, response_code (int), data (Dict)
**Server Info:**
- `ServerInfoUser`: id, name, user_level=0, address, real_name, country, avatar_bmp (bytes), server_id, session_id, accountage_secs, email, privlevel
- `ServerInfoDeckStorageFile`: creation_time
- `ServerInfoDeckStorageFolder`: items (List[Dict])
- `ServerInfoDeckStorageTreeItem`: id, name, file, folder
- `ServerInfoCard`: id, name, x, y, face_down=False, tapped=False, attacking=False, color, pt, annotation, destroy_on_zone_change=False, doesnt_untap=False, counter_list (List[Dict]), attach_player_id, attach_zone, attach_card_id, provider_id
- `ServerInfoZone`: name, zone_type=0, with_coords=False, card_count=0, card_list (List[ServerInfoCard]), always_reveal_top_card=False, always_look_at_top_card=False
- `ServerInfoGame`: server_id, room_id, game_id, description, with_password=False, max_players=4, game_types (List[int]), creator_info, only_buddies=False, only_registered=False, spectators_allowed=False, spectators_need_password=False, spectators_can_chat=False, spectators_omniscient=False, share_decklists_on_load=False, player_count=0, spectators_count=0, started=False, start_time, closed=False
---
### 2.7 Protocol Constants (`protocol_constants.py`)
**Enums:**
- `SessionCommandType` (IntEnum): PING=1000, LOGIN=1001, MESSAGE=1002, LIST_USERS=1003, GET_GAMES_OF_USER=1004, GET_USER_INFO=1005, ADD_TO_LIST=1006, REMOVE_FROM_LIST=1007, DECK_LIST=1008, DECK_NEW_DIR=1009, DECK_DEL_DIR=1010, DECK_DEL=1011, DECK_DOWNLOAD=1012, DECK_UPLOAD=1013, LIST_ROOMS=1014, JOIN_ROOM=1015, REGISTER=1016, ACTIVATE=1017, ACCOUNT_EDIT=1018, ACCOUNT_IMAGE=1019, ACCOUNT_PASSWORD=1020, FORGOT_PASSWORD_REQUEST=1021, FORGOT_PASSWORD_RESET=1022, FORGOT_PASSWORD_CHALLENGE=1023, REQUEST_PASSWORD_SALT=1024, SET_CARD_ART_PARAMS=1025, REPLAY_LIST=1100, REPLAY_DOWNLOAD=1101, REPLAY_MODIFY_MATCH=1102, REPLAY_DELETE_MATCH=1103, REPLAY_GET_CODE=1104, REPLAY_SUBMIT_CODE=1105
- `GameCommandType` (IntEnum): KICK_FROM_GAME=1000, LEAVE_GAME=1001, GAME_SAY=1002, SHUFFLE=1003, MULLIGAN=1004, ROLL_DIE=1005, DRAW_CARDS=1006, UNDO_DRAW=1007, FLIP_CARD=1008, ATTACH_CARD=1009, CREATE_TOKEN=1010, CREATE_ARROW=1011, DELETE_ARROW=1012, SET_CARD_ATTR=1013, SET_CARD_COUNTER=1014, INC_CARD_COUNTER=1015, READY_START=1016, CONCEDE=1017, INC_COUNTER=1018, CREATE_COUNTER=1019, SET_COUNTER=1020, DEL_COUNTER=1021, NEXT_TURN=1022, SET_ACTIVE_PHASE=1023, DUMP_ZONE=1024, REVEAL_CARDS=1026, MOVE_CARD=1027, SET_SIDEBOARD_PLAN=1028, DECK_SELECT=1029, SET_SIDEBOARD_LOCK=1030, CHANGE_ZONE_PROPERTIES=1031, UNCONCEDE=1032, JUDGE=1033, REVERSE_TURN=1034
- `GameEventType` (IntEnum): JOIN=1000, LEAVE=1001, GAME_CLOSED=1002, GAME_HOST_CHANGED=1003, KICKED=1004, GAME_STATE_CHANGED=1005, PLAYER_PROPERTIES_CHANGED=1007, GAME_SAY=1009, CREATE_ARROW=2000, DELETE_ARROW=2001, CREATE_COUNTER=2002, SET_COUNTER=2003, DEL_COUNTER=2004, DRAW_CARDS=2005, REVEAL_CARDS=2006, SHUFFLE=2007, ROLL_DIE=2008, MOVE_CARD=2009, FLIP_CARD=2010, DESTROY_CARD=2011, ATTACH_CARD=2012, CREATE_TOKEN=2013, SET_CARD_ATTR=2014, SET_CARD_COUNTER=2015, SET_ACTIVE_PLAYER=2016, SET_ACTIVE_PHASE=2017, DUMP_ZONE=2018, CHANGE_ZONE_PROPERTIES=2020, REVERSE_TURN=2021, GAME_LOG_NOTICE=2022
- `ResponseCode` (IntEnum): RespNotConnected=-1, RespNothing=0, RespOk=1, RespNotInRoom=2, RespInternalError=3, RespInvalidCommand=4, RespInvalidData=5, RespNameNotFound=6, RespLoginNeeded=7, RespFunctionNotAllowed=8, RespGameNotStarted=9, RespGameFull=10, RespContextError=11, RespWrongPassword=12, RespSpectatorsNotAllowed=13, RespOnlyBuddies=14, RespUserLevelTooLow=15, RespInIgnoreList=16, RespWouldOverwriteOldSession=17, RespChatFlood=18, RespUserIsBanned=19, RespAccessDenied=20, RespUsernameInvalid=21, RespRegistrationRequired=22, RespRegistrationAccepted=23, RespUserAlreadyExists=24, RespEmailRequiredToRegister=25, RespTooManyRequests=26, RespPasswordTooShort=27, RespAccountNotActivated=28, RespRegistrationDisabled=29, RespRegistrationFailed=30, RespActivationAccepted=31, RespActivationFailed=32, RespRegistrationAcceptedNeedsActivation=33, RespClientIdRequired=34, RespClientUpdateRequired=35, RespServerFull=36, RespEmailBlackListed=37
- `ZoneType` (IntEnum): PrivateZone=0, PublicZone=1, HiddenZone=2
- `UserLevelFlag` (IntFlag): IsNothing=0, IsUser=1, IsRegistered=2, IsModerator=4, IsAdmin=8, IsJudge=16
---
### 2.8 User Card Collection Schemas (`user_card_collection.py`)
**Enums:**
- `CardCondition`: NEAR_MINT, LIGHTLY_PLAYED, MODERATELY_PLAYED, HEAVILY_PLAYED, DAMAGED
- `AcquisitionMethod`: PACK_OPENING, TRADE, PURCHASE, GIFT, CONTEST, OTHER
**Card Collection Schemas:**
- `CardCollectionCreate`: card_id, quantity=1 (ge=1), condition=CardCondition.NEAR_MINT, language="EN" (max 5), is_foil=False, is_alt_art=False, acquired_date, acquisition_method, notes
- `CardCollectionUpdate`: quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes (optional)
- `CardCollectionResponse`: id, user_id, card_id, quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes, created_at, updated_at (from_attributes)
- `CardCollectionListResponse`: cards (List[CardCollectionResponse]), total, page, page_size, total_pages
**Wishlist Schemas:**
- `WishlistCreate`: card_id, max_price, notes
- `WishlistUpdate`: max_price, notes (optional)
- `WishlistResponse`: id, user_id, card_id, max_price, notes, created_at (from_attributes)
- `WishlistListResponse`: items (List[WishlistResponse]), total
**Collection Statistics Schemas:**
- `CollectionStatistics`: total_cards, unique_cards, total_quantity, foil_count, alt_art_count, condition_breakdown (Dict), language_breakdown (Dict), acquisition_breakdown (Dict)
- `CollectionSummaryResponse`: statistics (CollectionStatistics), recent_acquisitions (List[CardCollectionResponse]), top_cards (List[CardCollectionResponse])
**Generic Schemas:**
- `MessageResponse`: message (str)
- `CountResponse`: count (int)
- `ErrorDetail`: error (str), detail (str)
---
## 3. Routers (`app/routers/`)
### 3.1 Auth Router (`auth.py`)
**Endpoints:**
- `POST /login` - Authenticate user, return JWT tokens
- Request: LoginRequest
- Response: LoginResponse
- Dependencies: get_db (AsyncSession)
- Logic: Verify credentials, check account status, generate access/refresh tokens
- `POST /refresh` - Refresh access token
- Request: RefreshTokenRequest
- Response: TokenResponse
- Dependencies: get_db (AsyncSession)
- Logic: Validate refresh token, verify user still active, generate new access token
- `POST /register` - Register new user
- Request: UserCreate
- Response: UserResponse
- Dependencies: get_db (AsyncSession)
- Logic: Check username/email uniqueness, hash password, create user
- `GET /me` - Get current authenticated user
- Query: token (str)
- Response: UserResponse
- Dependencies: get_db (AsyncSession)
- Logic: Decode access token, fetch user
**Imports:**
- From `app.core.security`: verify_password, hash_password, create_access_token, create_refresh_token, decode_token
- From `app.models.models`: User
- From `app.schemas.schemas`: LoginRequest, LoginResponse, RefreshTokenRequest, TokenResponse, UserCreate, UserResponse
---
### 3.2 Users Router (`users.py`)
**Endpoints:**
- `GET /{user_id}` - Get user by ID
- Response: UserResponse
- Dependencies: get_db, get_current_user
- Logic: Fetch user, return response
- `PATCH /{user_id}` - Update user profile
- Request: UserUpdate
- Response: UserResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, update fields, hash new password if provided
- `POST /{user_id}/ban` - Ban user (admin only)
- Query: reason (str), expiration_time (optional str)
- Response: dict with message
- Dependencies: get_db, get_current_user
- Logic: Check admin privileges, update ban status
- `POST /{user_id}/unban` - Unban user (admin only)
- Response: dict with message
- Dependencies: get_db, get_current_user
- Logic: Check admin privileges, clear ban status
**Imports:**
- From `app.core.security`: get_current_user, hash_password
- From `app.models.models`: User
- From `app.schemas.schemas`: UserUpdate, UserResponse
---
### 3.3 Decks Router (`decks.py`)
**Deck CRUD:**
- `GET /` - List user's decks with filtering
- Query: status_filter, folder_id, is_precedent, page, page_size
- Response: UserDeckListResponse
- Dependencies: get_db, get_current_user
- Logic: Filter decks, count cards, paginate
- `POST /` - Create new user deck (DRAFT)
- Request: UserDeckCreate
- Response: UserDeckResponse (201)
- Dependencies: get_db, get_current_user
- Logic: Verify folder, create deck
- `GET /{deck_id}` - Get specific deck
- Response: UserDeckResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, count cards
- `PATCH /{deck_id}` - Update deck
- Request: UserDeckUpdate
- Response: UserDeckResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, check not FINAL, update fields
- `DELETE /{deck_id}` - Delete deck
- Response: MessageResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, delete
**Deck Finalize:**
- `POST /{deck_id}/finalize` - Transition DRAFT to FINAL
- Response: DeckFinalizeResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, check not already FINAL, verify has cards, update status
**Deck Card Management:**
- `POST /{deck_id}/cards` - Add card to deck
- Request: DeckCardCreate
- Response: DeckCardResponse (201)
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, check not FINAL, verify card exists, handle duplicates (update quantity or create new)
- `GET /{deck_id}/cards` - Get all cards in deck
- Query: zone (optional)
- Response: DeckCardListResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, fetch cards with details from mirror
- `PATCH /{deck_id}/cards/{card_id}` - Update card in deck
- Request: DeckCardUpdate
- Response: DeckCardResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, check not FINAL, update quantity/zone/position
- `DELETE /{deck_id}/cards/{card_id}` - Remove card from deck
- Response: MessageResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, check not FINAL, delete card entry
**Deck Precedents:**
- `GET /precedents` - List available precedents
- Query: page, page_size, format_filter
- Response: PrecedentListResponse
- Dependencies: get_db, get_current_user
- Logic: Filter public precedents, count cards, paginate
- `POST /precedents` - Create precedent (template)
- Request: PrecedentCreate
- Response: PrecedentResponse (201)
- Dependencies: get_db, get_current_user
- Logic: Create precedent with creator
- `GET /precedents/{precedent_id}` - Get specific precedent
- Response: PrecedentResponse
- Dependencies: get_db, get_current_user
- Logic: Fetch precedent, count cards
- `POST /precedents/{precedent_id}/use` - Clone precedent to new deck
- Response: dict with deck_id, deck_name, card_count
- Dependencies: get_db, get_current_user
- Logic: Fetch precedent, create new deck, copy cards
**Card Search:**
- `POST /search/cards` - Search MTG cards
- Request: CardSearchRequest
- Response: CardSearchResponse
- Dependencies: get_db, get_current_user
- Logic: Search local mirror by name/type_line/mana_cost with ILIKE
**Card Suggestions:**
- `GET /{deck_id}/suggestions` - Get card suggestions
- Query: suggestion_type (optional)
- Response: SuggestionListResponse
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, fetch suggestions with card names
- `POST /{deck_id}/suggestions` - Add suggestion
- Request: SuggestionCreate
- Response: SuggestionResponse (201)
- Dependencies: get_db, get_current_user
- Logic: Verify ownership, check not FINAL, create suggestion
**Imports:**
- From `app.core.database`: get_db, mtg_get_db
- From `app.core.security`: get_current_user
- From `app.models.models`: User, DecklistFolder, MtgonlineCard
- From `app.models.mtg_models`: MtgCard, MtgSet
- From `app.models.user_deck`: UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
- From `app.schemas.user_deck_schemas`: All deck-related schemas
---
### 3.4 Other Routers
**Rooms Router (`rooms.py`)**
- File exists but content not fully read
**Games Router (`games/`)**
- Directory exists (sub-routers)
**Admin Router (`admin.py`)**
- File exists but content not fully read
**Card Router (`card_router.py`)**
- File exists but content not fully read
**Interactions Router (`interactions.py`)**
- File exists but content not fully read
**Refresh Router (`refresh.py`)**
- File exists but content not fully read
**Card Import Router (`card_import.py`)**
- File exists but content not fully read
**Users Router (`users.py`)**
- Already documented above
**WS Router (`ws.py`)**
- File exists but content not fully read
---
## 4. Services (`app/services/`)
### 4.1 Card Database Service (`card_database.py`)
- File exists but content not fully read
### 4.2 Card Mirror Service (`card_mirror_service.py`)
- File exists but content not fully read
### 4.3 Card Search Service (`card_search_service.py`)
- File exists but content not fully read
### 4.4 Deck Manager Service (`deck_manager.py`)
- File exists but content not fully read
### 4.5 Deck Parser Service (`deck_parser.py`)
- File exists but content not fully read
### 4.6 Deck Suggestion Service (`deck_suggestion_service.py`)
- File exists but content not fully read
### 4.7 File Parser Service (`file_parser.py`)
- File exists but content not fully read
### 4.8 Fuzzy Card Matcher Service (`fuzzy_card_matcher.py`)
- File exists but content not fully read
### 4.9 Game Server Service (`game_server.py`)
- File exists but content not fully read
### 4.10 Import Batch Processor Service (`import_batch_processor.py`)
- File exists but content not fully read
### 4.11 MTGJSON Downloader Service (`mtgjson_downloader.py`)
- File exists but content not fully read
### 4.12 MTGJSON Loader Service (`mtgjson_loader.py`)
- File exists but content not fully read
### 4.13 MTGJSON Manager Service (`mtgjson_manager.py`)
- File exists but content not fully read
### 4.14 MTGJSON Uploader Service (`mtgjson_uploader.py`)
- File exists but content not fully read
---
## 5. Core Configuration (`app/core/`)
### 5.1 Database (`database.py`)
- File exists but content not fully read
### 5.2 Redis Client (`redis_client.py`)
- File exists but content not fully read
### 5.3 Security (`security.py`)
- File exists but content not fully read
### 5.4 Settings (`settings.py`)
- File exists but content not fully read
---
## 6. Main Application (`app/main.py`)
- File exists but content not fully read
---
## 7. Alembic Migrations (`alembic/`)
- Migration directory exists
- Migration files present but not fully documented
---
## 8. Test Structure (`tests/`)
- Test directory exists
- Test files present but not fully documented
---
## Summary
The MTG Online backend is a comprehensive FastAPI application with:
**Data Layer:**
- 20+ SQLAlchemy models across 6 model files
- Core user/deck/room management
- MTG card database integration (mtgjson.com)
- Card mirroring for performance
- User deck building with precedents
- Game replay and statistics
- Social features (groups, networks)
- Card collection and wishlist
**API Layer:**
- 3+ fully documented routers (auth, users, decks)
- 7+ additional routers (rooms, games, admin, card_router, interactions, refresh, card_import, ws)
- Comprehensive Pydantic schemas (8 schema files)
- Protocol buffer message definitions
- Protocol constants for MTG Online client
**Service Layer:**
- 14 service files for business logic
- Card database management
- Deck parsing and management
- Card search and fuzzy matching
- MTGJSON data loading and synchronization
- File import processing
**Infrastructure:**
- Async SQLAlchemy with PostgreSQL
- Redis for caching/sessions
- JWT authentication
- Alembic migrations
- Docker deployment
**Note:** This documentation covers the structure that was explicitly read. Several files (services, core configs, additional routers) exist but their detailed contents were not fully read in this session.