# 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.