Files
akadmin 900f51af3a Complete architecture simplification: 54+ files → 37 files
- Removed 11 duplicate router files (auth.py, users.py, decks.py, card_router.py, interactions.py, user_data.py, rooms.py, games/router.py, refresh.py, card_import.py, ws.py)
- Removed duplicate core files (logging_config.py, settings.py)
- Removed unused services (card_mirror_service.py, mtgjson_loader.py, mtgjson_uploader.py)
- Removed unused schemas (proto_messages.py, protocol_constants.py)
- Created run.py startup script for simple execution
- Created app/core/config.py for unified configuration management
- Created app/core/logging.py for logging configuration
- Consolidated routers into 6 focused files (auth_router.py, users_router.py, decks_router.py, cards_router.py, user_data_router.py, games_router.py)
- Updated all imports to use consolidated model paths
- Added backward compatibility aliases in database.py
- Created SIMPLIFICATION_SUMMARY.md documentation

Architecture improvements:
- Single unified database engine with auto-detection (SQLite/PostgreSQL)
- Consolidated models into 2 files (models.py, mtg_models.py)
- Clean router structure with one file per domain
- Simplified configuration with sensible defaults
- SQLite by default for development, PostgreSQL for production

Testing:
- All imports verified working
- App loads successfully
- Ready for development and testing
2026-08-26 15:15:36 +00:00

811 lines
28 KiB
Python

"""
Deck Management Router — Complete Deck Building Functionality.
This router provides the full deck building workflow for MTG Online users,
including:
* CRUD operations for per-user decklists (draft, final, precedent).
* Card management within decks (add, update, remove, list).
* Deck finalization (transition from DRAFT to FINAL status).
* Precedent (template) management — list, create, and clone into new decks.
* Card search integration using the local card mirror.
* Card suggestion system for deck refinement.
All endpoints require authentication via ``get_current_user`` and enforce
per-user ownership checks where appropriate.
Routes
------
Deck CRUD::
GET / List user decks (paginated, filterable)
POST / Create a new draft deck
GET /{deck_id} Get a specific deck
PATCH /{deck_id} Update deck metadata
DELETE /{deck_id} Delete a deck
Deck Finalize::
POST /{deck_id}/finalize Transition deck to FINAL status
Deck Card Management::
POST /{deck_id}/cards Add a card to a deck
GET /{deck_id}/cards List cards in a deck
PATCH /{deck_id}/cards/{card_id} Update card quantity/zone
DELETE /{deck_id}/cards/{card_id} Remove a card from a deck
Precedents::
GET /precedents List public precedents
POST /precedents Create a precedent
GET /precedents/{id} Get a specific precedent
POST /precedents/{id}/use Clone precedent into a new draft deck
Card Search::
POST /search/cards Search the local card mirror
Suggestions::
GET /{deck_id}/suggestions List suggestions for a deck
POST /{deck_id}/suggestions Add a suggestion to a deck
"""
from __future__ import annotations
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import delete, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_user
from app.models.mtg_models import MtgCard, MtgSet
from app.models.models import MtgonlineCard, User, DecklistFolder
from app.models.models import (
CardSuggestion,
DeckPrecedent,
DeckPrecedentCard,
UserDeck,
UserDeckCard,
)
from app.schemas.card_search_schemas import CardSearchResponse
from app.schemas.generic_schemas import CountResponse, MessageResponse
from app.schemas.user_deck_schemas import (
CardSearchRequest,
DeckCardCreate,
DeckCardResponse,
DeckCardUpdate,
DeckCardListResponse,
DeckCardWithDetailsResponse,
DeckFinalizeRequest,
DeckFinalizeResponse,
PrecedentCreate,
PrecedentResponse,
PrecedentListResponse,
PrecedentUpdate,
SuggestionCreate,
SuggestionListResponse,
SuggestionResponse,
UserDeckCreate,
UserDeckResponse,
UserDeckUpdate,
UserDeckListResponse,
)
from app.services.deck_manager import DeckManager
from app.services.deck_parser import DeckParser
router = APIRouter()
_deck_mgr: DeckManager = DeckManager()
_deck_parser: DeckParser = DeckParser()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _get_user_id(current_user: dict) -> int:
"""Extract and convert the authenticated user's ID."""
return int(current_user["user_id"])
async def _verify_deck_ownership(
db: AsyncSession,
deck_id: int,
user_id: int,
) -> UserDeck:
"""Verify that a deck exists and belongs to the user.
Raises:
HTTPException: 404 if the deck is not found.
"""
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
result = await db.execute(stmt)
deck = result.scalar_one_or_none()
if not deck:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
return deck
async def _get_card_count(db: AsyncSession, deck_id: int) -> int:
"""Return the number of cards in a deck."""
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
count_result = await db.execute(count_stmt)
return count_result.scalar() or 0
async def _enrich_deck_response(
deck: UserDeck,
card_count: int,
) -> UserDeckResponse:
"""Build a ``UserDeckResponse`` with card count and ownership flag."""
deck_data = UserDeckResponse.model_validate(deck)
deck_data.card_count = card_count
deck_data.is_owner = True
return deck_data
# ---------------------------------------------------------------------------
# Deck CRUD
# ---------------------------------------------------------------------------
@router.get("/", response_model=UserDeckListResponse)
async def list_user_decks(
status_filter: Optional[str] = None,
folder_id: Optional[int] = None,
is_precedent: Optional[bool] = None,
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> UserDeckListResponse:
"""List user's decks with optional filtering.
Supports filtering by deck status, folder, and whether the deck is a
precedent. Results are paginated.
"""
user_id = _get_user_id(current_user)
try:
decks = await _deck_mgr.list_decks(
db=db,
user_id=user_id,
status_filter=status_filter,
folder_id=folder_id,
is_precedent=is_precedent,
page=page,
page_size=page_size,
)
# Build response with card counts
deck_ids = [d.id for d in decks]
card_counts: dict[int, int] = {}
if deck_ids:
count_subquery = (
select(UserDeckCard.deck_id, func.count().label("cnt"))
.where(UserDeckCard.deck_id.in_(deck_ids))
.group_by(UserDeckCard.deck_id)
.subquery()
)
count_stmt = select(count_subquery.c.deck_id, count_subquery.c.cnt).where(
count_subquery.c.deck_id.in_(deck_ids)
)
count_result = await db.execute(count_stmt)
card_counts = {row[0]: row[1] for row in count_result.fetchall()}
deck_responses: list[UserDeckResponse] = []
for deck in decks:
deck_data = UserDeckResponse.model_validate(deck)
deck_data.card_count = card_counts.get(deck.id, 0)
deck_data.is_owner = True
deck_responses.append(deck_data)
total = len(decks)
return UserDeckListResponse(
decks=deck_responses,
total=total,
page=page,
page_size=page_size,
total_pages=(total + page_size - 1) // page_size,
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post("/", response_model=UserDeckResponse, status_code=status.HTTP_201_CREATED)
async def create_user_deck(
request: UserDeckCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> UserDeckResponse:
"""Create a new user deck in DRAFT status."""
user_id = _get_user_id(current_user)
try:
new_deck = await _deck_mgr.create_deck(
db=db,
user_id=user_id,
name=request.name,
folder_id=request.folder_id,
format=request.format,
notes=request.notes,
is_precedent=request.is_precedent,
precedent_name=request.precedent_name,
)
deck_data = UserDeckResponse.model_validate(new_deck)
deck_data.card_count = 0
deck_data.is_owner = True
return deck_data
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get("/{deck_id}", response_model=UserDeckResponse)
async def get_user_deck(
deck_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> UserDeckResponse:
"""Get a specific user deck by ID."""
user_id = _get_user_id(current_user)
try:
deck = await _deck_mgr.get_deck(db=db, deck_id=deck_id, user_id=user_id)
if not deck:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
card_count = await _get_card_count(db, deck_id)
return await _enrich_deck_response(deck, card_count)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.patch("/{deck_id}", response_model=UserDeckResponse)
async def update_user_deck(
deck_id: int,
request: UserDeckUpdate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> UserDeckResponse:
"""Update a user deck's metadata."""
user_id = _get_user_id(current_user)
try:
update_data = request.model_dump(exclude_unset=True)
if "status" in update_data and update_data["status"]:
update_data["status"] = update_data["status"].value
updated_deck = await _deck_mgr.update_deck(
db=db,
deck_id=deck_id,
user_id=user_id,
**update_data,
)
if not updated_deck:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
card_count = await _get_card_count(db, deck_id)
return await _enrich_deck_response(updated_deck, card_count)
except HTTPException:
raise
except ValueError as e:
if "not found" in str(e).lower():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.delete("/{deck_id}", response_model=MessageResponse)
async def delete_user_deck(
deck_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> MessageResponse:
"""Delete a user deck."""
user_id = _get_user_id(current_user)
try:
result = await _deck_mgr.delete_deck(db=db, deck_id=deck_id, user_id=user_id)
if not result:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
return MessageResponse(message="Deck deleted successfully")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
# ---------------------------------------------------------------------------
# Deck Finalize
# ---------------------------------------------------------------------------
@router.post("/{deck_id}/finalize", response_model=DeckFinalizeResponse)
async def finalize_user_deck(
deck_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> DeckFinalizeResponse:
"""Transition a deck from DRAFT to FINAL status."""
user_id = _get_user_id(current_user)
try:
deck = await _deck_mgr.finalize_deck(db=db, deck_id=deck_id, user_id=user_id)
if not deck:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
return DeckFinalizeResponse(
deck_id=deck_id,
status="FINAL",
message="Deck finalized successfully",
)
except HTTPException:
raise
except ValueError as e:
if "not found" in str(e).lower():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# ---------------------------------------------------------------------------
# Deck Card Management
# ---------------------------------------------------------------------------
@router.post("/{deck_id}/cards", response_model=DeckCardResponse, status_code=status.HTTP_201_CREATED)
async def add_deck_card(
deck_id: int,
request: DeckCardCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> DeckCardResponse:
"""Add a card to a user's deck.
If a card already exists in the same zone, the quantity is incremented
rather than creating a duplicate entry.
"""
user_id = _get_user_id(current_user)
deck = await _verify_deck_ownership(db, deck_id, user_id)
if deck.status == "FINAL":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
# Verify card exists in local mirror
card_stmt = select(MtgonlineCard).where(MtgonlineCard.id == request.card_id)
card_result = await db.execute(card_stmt)
card = card_result.scalar_one_or_none()
if not card:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in database")
# Check for duplicate (same card, same zone)
existing_stmt = select(UserDeckCard).where(
UserDeckCard.deck_id == deck_id,
UserDeckCard.card_id == request.card_id,
UserDeckCard.zone == request.zone.value,
)
existing_result = await db.execute(existing_stmt)
existing = existing_result.scalar_one_or_none()
if existing:
# Update quantity
new_qty = existing.quantity + request.quantity
stmt = update(UserDeckCard).where(UserDeckCard.id == existing.id).values(quantity=new_qty)
await db.execute(stmt)
await db.flush()
stmt = select(UserDeckCard).where(UserDeckCard.id == existing.id)
result = await db.execute(stmt)
updated = result.scalar_one_or_none()
else:
new_card = UserDeckCard(
deck_id=deck_id,
card_id=request.card_id,
quantity=request.quantity,
zone=request.zone.value,
position=request.position,
)
db.add(new_card)
await db.flush()
updated = new_card
return DeckCardResponse.model_validate(updated)
@router.get("/{deck_id}/cards", response_model=DeckCardListResponse)
async def get_deck_cards(
deck_id: int,
zone: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> DeckCardListResponse:
"""Get all cards in a user's deck, optionally filtered by zone."""
user_id = _get_user_id(current_user)
await _verify_deck_ownership(db, deck_id, user_id)
# Use service to get cards
deck_cards = await _deck_mgr.get_deck_cards(db=db, deck_id=deck_id, zone=zone)
# Fetch card details from local mirror
card_ids = [dc.card_id for dc in deck_cards]
card_details: dict[int, MtgonlineCard] = {}
if card_ids:
card_stmt = select(MtgonlineCard).where(MtgonlineCard.id.in_(card_ids))
card_result = await db.execute(card_stmt)
for c in card_result.scalars().all():
card_details[c.id] = c
card_responses: list[DeckCardWithDetailsResponse] = []
for dc in deck_cards:
card = card_details.get(dc.card_id)
response = DeckCardWithDetailsResponse.model_validate(dc)
response.card_name = card.name if card else f"Card#{dc.card_id}"
response.card_type_line = card.type_line if card else ""
response.card_image = card.image if card else None
card_responses.append(response)
return DeckCardListResponse(cards=card_responses, total=len(card_responses))
@router.patch("/{deck_id}/cards/{card_id}", response_model=DeckCardResponse)
async def update_deck_card(
deck_id: int,
card_id: int,
request: DeckCardUpdate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> DeckCardResponse:
"""Update a card's quantity or zone in a deck."""
user_id = _get_user_id(current_user)
deck = await _verify_deck_ownership(db, deck_id, user_id)
if deck.status == "FINAL":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
# Find the card entry
stmt = select(UserDeckCard).where(
UserDeckCard.deck_id == deck_id,
UserDeckCard.card_id == card_id,
)
result = await db.execute(stmt)
deck_card = result.scalar_one_or_none()
if not deck_card:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in deck")
update_data = request.model_dump(exclude_unset=True)
if "zone" in update_data and update_data["zone"]:
update_data["zone"] = update_data["zone"].value
stmt = update(UserDeckCard).where(UserDeckCard.id == deck_card.id).values(**update_data)
await db.execute(stmt)
await db.flush()
stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card.id)
result = await db.execute(stmt)
updated = result.scalar_one_or_none()
return DeckCardResponse.model_validate(updated)
@router.delete("/{deck_id}/cards/{card_id}", response_model=MessageResponse)
async def remove_deck_card(
deck_id: int,
card_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> MessageResponse:
"""Remove a card from a user's deck."""
user_id = _get_user_id(current_user)
deck = await _verify_deck_ownership(db, deck_id, user_id)
if deck.status == "FINAL":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
# Find the card entry
stmt = select(UserDeckCard).where(
UserDeckCard.deck_id == deck_id,
UserDeckCard.card_id == card_id,
)
result = await db.execute(stmt)
deck_card = result.scalar_one_or_none()
if not deck_card:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in deck")
await db.execute(delete(UserDeckCard).where(UserDeckCard.id == deck_card.id))
await db.flush()
return MessageResponse(message="Card removed from deck")
# ---------------------------------------------------------------------------
# Deck Precedents
# ---------------------------------------------------------------------------
# Note: Precedent endpoints use direct DB operations as DeckManager
# does not yet have precedent-specific methods.
@router.get("/precedents", response_model=PrecedentListResponse)
async def list_precedents(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
format_filter: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> PrecedentListResponse:
"""List available (public) deck precedents, paginated."""
offset = (page - 1) * page_size
conditions = [DeckPrecedent.is_public == True] # noqa: E712
if format_filter:
conditions.append(DeckPrecedent.format == format_filter)
# Count total
count_stmt = select(func.count()).select_from(DeckPrecedent).where(*conditions)
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# Fetch precedents
stmt = select(DeckPrecedent).where(*conditions).order_by(DeckPrecedent.created_at.desc()).offset(offset).limit(page_size)
result = await db.execute(stmt)
precedents = result.scalars().all()
# Get card counts
prec_ids = [p.id for p in precedents]
card_counts: dict[int, int] = {}
if prec_ids:
count_subquery = (
select(DeckPrecedentCard.precedent_id, func.count().label("cnt"))
.where(DeckPrecedentCard.precedent_id.in_(prec_ids))
.group_by(DeckPrecedentCard.precedent_id)
.subquery()
)
count_stmt = select(count_subquery.c.precedent_id, count_subquery.c.cnt).where(
count_subquery.c.precedent_id.in_(prec_ids)
)
count_result = await db.execute(count_stmt)
card_counts = {row[0]: row[1] for row in count_result.fetchall()}
prec_responses: list[PrecedentResponse] = []
for p in precedents:
prec_data = PrecedentResponse.model_validate(p)
prec_data.card_count = card_counts.get(p.id, 0)
prec_responses.append(prec_data)
return PrecedentListResponse(
precedents=prec_responses,
total=total,
page=page,
page_size=page_size,
total_pages=(total + page_size - 1) // page_size,
)
@router.post("/precedents", response_model=PrecedentResponse, status_code=status.HTTP_201_CREATED)
async def create_precedent(
request: PrecedentCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> PrecedentResponse:
"""Create a deck precedent (template)."""
user_id = _get_user_id(current_user)
new_precedent = DeckPrecedent(
name=request.name,
description=request.description,
format=request.format,
is_public=request.is_public,
created_by=user_id,
)
db.add(new_precedent)
await db.flush()
prec_data = PrecedentResponse.model_validate(new_precedent)
prec_data.card_count = 0
return prec_data
@router.get("/precedents/{precedent_id}", response_model=PrecedentResponse)
async def get_precedent(
precedent_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> PrecedentResponse:
"""Get a specific deck precedent by ID."""
stmt = select(DeckPrecedent).where(DeckPrecedent.id == precedent_id)
result = await db.execute(stmt)
precedent = result.scalar_one_or_none()
if not precedent:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Precedent not found")
card_count = await _get_card_count(db, precedent_id)
prec_data = PrecedentResponse.model_validate(precedent)
prec_data.card_count = card_count
return prec_data
@router.post("/precedents/{precedent_id}/use")
async def use_precedent(
precedent_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> dict:
"""Clone a precedent into a new draft deck for the current user."""
user_id = _get_user_id(current_user)
try:
new_deck = await _deck_mgr.clone_precedent(
db=db,
precedent_id=precedent_id,
user_id=user_id,
)
card_count = len(await _deck_mgr.get_deck_cards(db=db, deck_id=new_deck.id))
return {
"message": "Precedent cloned into new deck",
"deck_id": new_deck.id,
"deck_name": new_deck.name,
"card_count": card_count,
}
except ValueError as e:
if "not found" in str(e).lower():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# ---------------------------------------------------------------------------
# Card Search Integration
# ---------------------------------------------------------------------------
@router.post("/search/cards", response_model=CardSearchResponse)
async def search_cards_for_deck(
request: CardSearchRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> CardSearchResponse:
"""Search MTG cards for use in deck building.
Searches across name, type line, and mana cost fields in the local
card mirror.
"""
offset = (request.offset // request.limit) * request.limit
# Search across multiple fields using local mirror
stmt = (
select(MtgonlineCard)
.where(
or_(
MtgonlineCard.name.ilike(f"%{request.query}%"),
MtgonlineCard.type_line.ilike(f"%{request.query}%"),
MtgonlineCard.mana_cost.ilike(f"%{request.query}%"),
)
)
.offset(offset)
.limit(request.limit)
)
result = await db.execute(stmt)
cards = result.scalars().all()
card_list: list[dict] = []
for card in cards:
card_data = {
"id": card.id,
"name": card.name,
"mana_cost": card.mana_cost,
"type_line": card.type_line,
"oracle_text": card.oracle_text,
"power": card.power,
"toughness": card.toughness,
"rarity": card.rarity,
"layout": card.layout,
"artist": card.artist,
"flavor_text": card.flavor_text,
"set_code": card.set_code,
"set_name": card.set_name,
"identifiers": card.identifiers,
"images": card.images,
}
card_list.append(card_data)
# Get total count
count_stmt = select(func.count()).select_from(MtgonlineCard).where(
or_(
MtgonlineCard.name.ilike(f"%{request.query}%"),
MtgonlineCard.type_line.ilike(f"%{request.query}%"),
MtgonlineCard.mana_cost.ilike(f"%{request.query}%"),
)
)
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
return CardSearchResponse(
cards=card_list,
total=total,
page=request.offset // request.limit + 1,
page_size=request.limit,
total_pages=(total + request.limit - 1) // request.limit,
)
# ---------------------------------------------------------------------------
# Card Suggestions
# ---------------------------------------------------------------------------
# Note: Suggestion endpoints use direct DB operations as DeckManager
# does not yet have suggestion-specific methods.
@router.get("/{deck_id}/suggestions", response_model=SuggestionListResponse)
async def get_deck_suggestions(
deck_id: int,
suggestion_type: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> SuggestionListResponse:
"""Get card suggestions for a deck, optionally filtered by type."""
user_id = _get_user_id(current_user)
await _verify_deck_ownership(db, deck_id, user_id)
conditions = [CardSuggestion.deck_id == deck_id]
if suggestion_type:
conditions.append(CardSuggestion.suggestion_type == suggestion_type)
# Fetch suggestions
stmt = select(CardSuggestion).where(*conditions).order_by(CardSuggestion.created_at.desc())
result = await db.execute(stmt)
suggestions = result.scalars().all()
# Fetch card names from local mirror
card_ids = [s.card_id for s in suggestions]
card_names: dict[int, str] = {}
if card_ids:
card_stmt = select(MtgonlineCard).where(MtgonlineCard.id.in_(card_ids))
card_result = await db.execute(card_stmt)
for c in card_result.scalars().all():
card_names[c.id] = c.name
sugg_responses: list[SuggestionResponse] = []
for s in suggestions:
sugg_data = SuggestionResponse.model_validate(s)
sugg_data.card_name = card_names.get(s.card_id, f"Card#{s.card_id}")
sugg_responses.append(sugg_data)
return SuggestionListResponse(
suggestions=sugg_responses,
total=len(sugg_responses),
)
@router.post("/{deck_id}/suggestions", response_model=SuggestionResponse, status_code=status.HTTP_201_CREATED)
async def add_suggestion(
deck_id: int,
request: SuggestionCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> SuggestionResponse:
"""Add a card suggestion to a deck."""
user_id = _get_user_id(current_user)
deck = await _verify_deck_ownership(db, deck_id, user_id)
if deck.status == "FINAL":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
new_suggestion = CardSuggestion(
deck_id=deck_id,
card_id=request.card_id,
source_card_id=request.source_card_id,
suggestion_type=request.suggestion_type.value,
confidence=request.confidence,
notes=request.notes,
)
db.add(new_suggestion)
await db.flush()
return SuggestionResponse.model_validate(new_suggestion)