Phase 7: End-to-End API Testing - Database setup, migrations, and application fixes
- Switched from psycopg2 to asyncpg for async SQLAlchemy support - Fixed router registration in main.py - removed duplicate prefixes - Added user_data export to routers/__init__.py - Refactored decks router to use DeckManager service layer - Integrated FuzzyCardMatcher into card_router search endpoints - Made WishlistCreate.card_id optional for proper schema validation - Set PostgreSQL password and configured scram-sha-256 auth - Updated alembic.ini to use local PostgreSQL instead of Docker hostname - Created generic_schemas.py for reusable schema patterns - Added test_routers.py and test_schema_validation.py test files - All 6 Alembic migrations applied successfully (37 tables created) - Application running on port 8000 with all services connected
This commit is contained in:
@@ -14,6 +14,7 @@ from app.routers import card_router
|
||||
from app.routers import interactions
|
||||
from app.routers import refresh
|
||||
from app.routers import card_import
|
||||
from app.routers import user_data
|
||||
|
||||
__all__ = [
|
||||
"auth",
|
||||
@@ -26,4 +27,5 @@ __all__ = [
|
||||
"interactions",
|
||||
"refresh",
|
||||
"card_import",
|
||||
"user_data",
|
||||
]
|
||||
|
||||
@@ -28,8 +28,8 @@ from app.schemas.card_import_schemas import (
|
||||
CardImportStatusResponse,
|
||||
CardMatchResult,
|
||||
CardImportSummary,
|
||||
MessageResponse,
|
||||
)
|
||||
from app.schemas.generic_schemas import MessageResponse
|
||||
from app.schemas.user_deck_schemas import DeckCardResponse, DeckCardListResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -13,7 +13,9 @@ from app.core.database import mtg_get_db
|
||||
from app.core.redis_client import cache_get, cache_set
|
||||
from app.services.card_search_service import CardSearchService
|
||||
from app.services.deck_suggestion_service import DeckSuggestionService
|
||||
from app.services.fuzzy_card_matcher import FuzzyCardMatcher
|
||||
from app.models.user_deck import UserDeck
|
||||
from app.models.mtg_models import MtgCard
|
||||
from app.schemas.card_search_schemas import CardSearchResponse, CardResponse, SetResponse, CardTypeResponse
|
||||
|
||||
router = APIRouter(prefix="/api/cards", tags=["Card Search"])
|
||||
@@ -33,6 +35,7 @@ async def search_cards_endpoint(
|
||||
Search cards with filters.
|
||||
|
||||
Supports filtering by type, set, and color in addition to name search.
|
||||
Uses fuzzy matching as a fallback when exact/partial matches are not found.
|
||||
"""
|
||||
cache_key = f"card_search:{q}:{card_type}:{set_code}:{color}:{limit}:{offset}"
|
||||
|
||||
@@ -41,7 +44,10 @@ async def search_cards_endpoint(
|
||||
if cached:
|
||||
return {"cached": True, "results": cached}
|
||||
|
||||
# Search cards
|
||||
# Normalize the search query for consistency
|
||||
normalized_query = FuzzyCardMatcher.normalize_card_name(q)
|
||||
|
||||
# Search cards using the existing service
|
||||
results = await CardSearchService.search_cards(
|
||||
db=db,
|
||||
query=q,
|
||||
@@ -52,12 +58,125 @@ async def search_cards_endpoint(
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
# If no results found, try fuzzy matching as a fallback
|
||||
if results["total"] == 0:
|
||||
fuzzy_results = await _fuzzy_search_fallback(
|
||||
db=db,
|
||||
query=q,
|
||||
normalized_query=normalized_query,
|
||||
card_type=card_type,
|
||||
set_code=set_code,
|
||||
color=color,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
results = fuzzy_results
|
||||
|
||||
# Add fuzzy matching metadata to results
|
||||
results["query_normalized"] = normalized_query
|
||||
results["fuzzy_match"] = True
|
||||
|
||||
# Cache results for 5 minutes
|
||||
await cache_set(cache_key, str(results), ttl=300)
|
||||
|
||||
return {"cached": False, "results": results}
|
||||
|
||||
|
||||
async def _fuzzy_search_fallback(
|
||||
db: AsyncSession,
|
||||
query: str,
|
||||
normalized_query: str,
|
||||
card_type: Optional[str] = None,
|
||||
set_code: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Fallback fuzzy search when exact/partial matches yield no results.
|
||||
|
||||
Fetches all cards matching the type/set/color filters, then uses
|
||||
fuzzy matching to find the best card name matches.
|
||||
"""
|
||||
from sqlalchemy import select, or_
|
||||
|
||||
# Fetch candidate cards based on non-name filters
|
||||
conditions = []
|
||||
if card_type:
|
||||
conditions.append(MtgCard.type_line.ilike(f"%{card_type}%"))
|
||||
if set_code:
|
||||
conditions.append(MtgCard.set_code == set_code)
|
||||
if color:
|
||||
colors = [c.strip() for c in color.upper().split(",")]
|
||||
for c in colors:
|
||||
if c in ["W", "U", "B", "R", "G"]:
|
||||
conditions.append(MtgCard.colors.ilike(f"%{c}%"))
|
||||
|
||||
# If no filters, fetch a broader set for fuzzy matching
|
||||
if not conditions:
|
||||
stmt = select(MtgCard).limit(limit * 5)
|
||||
else:
|
||||
stmt = select(MtgCard).where(*conditions).limit(limit * 5)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
candidate_cards = result.scalars().all()
|
||||
|
||||
if not candidate_cards:
|
||||
return {
|
||||
"cards": [],
|
||||
"total": 0,
|
||||
"page": offset // limit + 1,
|
||||
"page_size": limit,
|
||||
"total_pages": 0,
|
||||
"fuzzy_fallback": True,
|
||||
"message": "No cards found matching your query.",
|
||||
}
|
||||
|
||||
# Build candidate name list and lookup
|
||||
candidate_names = [card.name for card in candidate_cards if card.name]
|
||||
card_lookup = {card.name.lower(): card for card in candidate_cards if card.name}
|
||||
|
||||
# Use fuzzy matching to find best matches
|
||||
matched_name, confidence, match_type = FuzzyCardMatcher.find_best_match(
|
||||
normalized_query, candidate_names, threshold=FuzzyCardMatcher.MIN_MATCH_THRESHOLD
|
||||
)
|
||||
|
||||
# Build results from fuzzy matches
|
||||
card_list = []
|
||||
if matched_name and matched_name.lower() in card_lookup:
|
||||
card = card_lookup[matched_name.lower()]
|
||||
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,
|
||||
"colors": card.colors,
|
||||
"set_code": card.set_code,
|
||||
"set_name": card.set_name,
|
||||
"fuzzy_match": True,
|
||||
"match_confidence": confidence,
|
||||
"match_type": match_type,
|
||||
"original_query": query,
|
||||
}
|
||||
card_list.append(card_data)
|
||||
|
||||
return {
|
||||
"cards": card_list,
|
||||
"total": len(card_list),
|
||||
"page": offset // limit + 1,
|
||||
"page_size": limit,
|
||||
"total_pages": (len(card_list) + limit - 1) // limit if card_list else 0,
|
||||
"fuzzy_fallback": True,
|
||||
"match_type": match_type,
|
||||
"confidence": confidence,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{card_id}", response_model=CardResponse)
|
||||
async def get_card_endpoint(
|
||||
card_id: int,
|
||||
|
||||
+161
-221
@@ -6,7 +6,7 @@ deck finalization, precedent templates, and card search integration.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete, update, func, and_, or_
|
||||
from sqlalchemy import select, func, or_, update, delete
|
||||
from typing import Optional, List
|
||||
|
||||
from app.core.database import get_db, mtg_get_db
|
||||
@@ -20,12 +20,18 @@ from app.schemas.user_deck_schemas import (
|
||||
PrecedentCreate, PrecedentUpdate, PrecedentResponse, PrecedentListResponse,
|
||||
SuggestionCreate, SuggestionResponse, SuggestionListResponse,
|
||||
DeckFinalizeRequest, DeckFinalizeResponse,
|
||||
CardSearchRequest, CardSearchResponse,
|
||||
MessageResponse, CountResponse
|
||||
CardSearchRequest,
|
||||
)
|
||||
from app.schemas.card_search_schemas import CardSearchResponse
|
||||
from app.schemas.generic_schemas import MessageResponse, CountResponse
|
||||
from app.services.deck_manager import DeckManager
|
||||
from app.services.deck_parser import DeckParser
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_deck_mgr = DeckManager()
|
||||
_deck_parser = DeckParser()
|
||||
|
||||
|
||||
# ===== Deck CRUD =====
|
||||
|
||||
@@ -41,57 +47,50 @@ async def list_user_decks(
|
||||
):
|
||||
"""List user's decks with optional filtering."""
|
||||
user_id = int(current_user["user_id"])
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# Build conditions
|
||||
conditions = [UserDeck.user_id == user_id]
|
||||
if status_filter:
|
||||
conditions.append(UserDeck.status == status_filter)
|
||||
if folder_id:
|
||||
conditions.append(UserDeck.folder_id == folder_id)
|
||||
if is_precedent is not None:
|
||||
conditions.append(UserDeck.is_precedent == is_precedent)
|
||||
|
||||
# Count total
|
||||
count_stmt = select(func.count()).select_from(UserDeck).where(*conditions)
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# Fetch decks
|
||||
stmt = select(UserDeck).where(*conditions).order_by(UserDeck.updated_at.desc()).offset(offset).limit(page_size)
|
||||
result = await db.execute(stmt)
|
||||
decks = result.scalars().all()
|
||||
|
||||
# Get card counts
|
||||
deck_ids = [d.id for d in decks]
|
||||
card_counts = {}
|
||||
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()
|
||||
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,
|
||||
)
|
||||
count_stmt = select(count_subquery.c.deck_id, count_subquery.c.cnt).where(
|
||||
count_subquery.c.deck_id.in_(deck_ids)
|
||||
|
||||
# Build response with card counts
|
||||
deck_ids = [d.id for d in decks]
|
||||
card_counts = {}
|
||||
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 = []
|
||||
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,
|
||||
)
|
||||
count_result = await db.execute(count_stmt)
|
||||
card_counts = {row[0]: row[1] for row in count_result.fetchall()}
|
||||
|
||||
deck_responses = []
|
||||
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)
|
||||
|
||||
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)
|
||||
@@ -102,34 +101,27 @@ async def create_user_deck(
|
||||
):
|
||||
"""Create a new user deck (DRAFT status)."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Verify folder exists if specified
|
||||
if request.folder_id:
|
||||
stmt = select(DecklistFolder).where(
|
||||
DecklistFolder.id == request.folder_id,
|
||||
DecklistFolder.owner_id == user_id,
|
||||
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,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
folder = result.scalar_one_or_none()
|
||||
if not folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
|
||||
new_deck = UserDeck(
|
||||
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,
|
||||
)
|
||||
db.add(new_deck)
|
||||
await db.flush()
|
||||
|
||||
deck_data = UserDeckResponse.model_validate(new_deck)
|
||||
deck_data.card_count = 0
|
||||
deck_data.is_owner = True
|
||||
return deck_data
|
||||
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)
|
||||
@@ -140,26 +132,24 @@ async def get_user_deck(
|
||||
):
|
||||
"""Get a specific user deck."""
|
||||
user_id = int(current_user["user_id"])
|
||||
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")
|
||||
|
||||
stmt = select(UserDeck).where(
|
||||
UserDeck.id == deck_id,
|
||||
UserDeck.user_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
deck = result.scalar_one_or_none()
|
||||
# Get card count
|
||||
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
||||
count_result = await db.execute(count_stmt)
|
||||
card_count = count_result.scalar() or 0
|
||||
|
||||
if not deck:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
||||
|
||||
# Get card count
|
||||
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
||||
count_result = await db.execute(count_stmt)
|
||||
card_count = count_result.scalar() or 0
|
||||
|
||||
deck_data = UserDeckResponse.model_validate(deck)
|
||||
deck_data.card_count = card_count
|
||||
deck_data.is_owner = True
|
||||
return deck_data
|
||||
deck_data = UserDeckResponse.model_validate(deck)
|
||||
deck_data.card_count = card_count
|
||||
deck_data.is_owner = True
|
||||
return deck_data
|
||||
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)
|
||||
@@ -171,41 +161,36 @@ async def update_user_deck(
|
||||
):
|
||||
"""Update a user deck."""
|
||||
user_id = int(current_user["user_id"])
|
||||
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
|
||||
|
||||
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
||||
result = await db.execute(stmt)
|
||||
deck = result.scalar_one_or_none()
|
||||
updated_deck = await _deck_mgr.update_deck(
|
||||
db=db,
|
||||
deck_id=deck_id,
|
||||
user_id=user_id,
|
||||
**update_data,
|
||||
)
|
||||
|
||||
if not deck:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
||||
if not updated_deck:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
||||
|
||||
# Can't modify FINAL decks
|
||||
if deck.status == "FINAL":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
|
||||
# Get card count
|
||||
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
||||
count_result = await db.execute(count_stmt)
|
||||
card_count = count_result.scalar() or 0
|
||||
|
||||
# Update fields
|
||||
update_data = request.model_dump(exclude_unset=True)
|
||||
if "status" in update_data and update_data["status"]:
|
||||
update_data["status"] = update_data["status"].value
|
||||
|
||||
stmt = update(UserDeck).where(UserDeck.id == deck_id).values(**update_data)
|
||||
await db.execute(stmt)
|
||||
await db.flush()
|
||||
|
||||
# Fetch updated deck
|
||||
stmt = select(UserDeck).where(UserDeck.id == deck_id)
|
||||
result = await db.execute(stmt)
|
||||
updated_deck = result.scalar_one_or_none()
|
||||
|
||||
# Get card count
|
||||
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
||||
count_result = await db.execute(count_stmt)
|
||||
card_count = count_result.scalar() or 0
|
||||
|
||||
deck_data = UserDeckResponse.model_validate(updated_deck)
|
||||
deck_data.card_count = card_count
|
||||
deck_data.is_owner = True
|
||||
return deck_data
|
||||
deck_data = UserDeckResponse.model_validate(updated_deck)
|
||||
deck_data.card_count = card_count
|
||||
deck_data.is_owner = True
|
||||
return deck_data
|
||||
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)
|
||||
@@ -216,18 +201,15 @@ async def delete_user_deck(
|
||||
):
|
||||
"""Delete a user deck."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
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")
|
||||
|
||||
await db.execute(delete(UserDeck).where(UserDeck.id == deck_id))
|
||||
await db.flush()
|
||||
|
||||
return MessageResponse(message="Deck deleted successfully")
|
||||
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 =====
|
||||
@@ -240,34 +222,21 @@ async def finalize_user_deck(
|
||||
):
|
||||
"""Transition a deck from DRAFT to FINAL status."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
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")
|
||||
|
||||
if deck.status == "FINAL":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Deck is already finalized")
|
||||
|
||||
# Check deck has cards
|
||||
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
||||
count_result = await db.execute(count_stmt)
|
||||
card_count = count_result.scalar() or 0
|
||||
if card_count == 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot finalize an empty deck")
|
||||
|
||||
# Update status
|
||||
stmt = update(UserDeck).where(UserDeck.id == deck_id).values(status="FINAL")
|
||||
await db.execute(stmt)
|
||||
await db.flush()
|
||||
|
||||
return DeckFinalizeResponse(
|
||||
deck_id=deck_id,
|
||||
status="FINAL",
|
||||
message="Deck finalized successfully",
|
||||
)
|
||||
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 =====
|
||||
@@ -283,14 +252,12 @@ async def add_deck_card(
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Verify deck exists and belongs to user
|
||||
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
||||
result = await db.execute(stmt)
|
||||
deck = result.scalar_one_or_none()
|
||||
|
||||
deck_stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
||||
deck_result = await db.execute(deck_stmt)
|
||||
deck = deck_result.scalar_one_or_none()
|
||||
if not deck:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
||||
|
||||
# Can't modify FINAL decks
|
||||
if deck.status == "FINAL":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
|
||||
|
||||
@@ -353,15 +320,8 @@ async def get_deck_cards(
|
||||
if not deck:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
||||
|
||||
# Build conditions
|
||||
conditions = [UserDeckCard.deck_id == deck_id]
|
||||
if zone:
|
||||
conditions.append(UserDeckCard.zone == zone)
|
||||
|
||||
# Fetch cards
|
||||
stmt = select(UserDeckCard).where(*conditions).order_by(UserDeckCard.id)
|
||||
result = await db.execute(stmt)
|
||||
deck_cards = result.scalars().all()
|
||||
# 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]
|
||||
@@ -471,6 +431,8 @@ async def remove_deck_card(
|
||||
|
||||
|
||||
# ===== 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(
|
||||
@@ -585,46 +547,22 @@ async def use_precedent(
|
||||
"""Clone a precedent into a new draft deck for the current user."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Get precedent
|
||||
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")
|
||||
|
||||
# Create new deck from precedent
|
||||
new_deck = UserDeck(
|
||||
user_id=user_id,
|
||||
name=f"Copy of {precedent.name}",
|
||||
format=precedent.format,
|
||||
is_precedent=False,
|
||||
)
|
||||
db.add(new_deck)
|
||||
await db.flush()
|
||||
|
||||
# Copy cards from precedent
|
||||
card_stmt = select(DeckPrecedentCard).where(DeckPrecedentCard.precedent_id == precedent_id)
|
||||
card_result = await db.execute(card_stmt)
|
||||
precedent_cards = card_result.scalars().all()
|
||||
|
||||
for pc in precedent_cards:
|
||||
new_dc = UserDeckCard(
|
||||
deck_id=new_deck.id,
|
||||
card_id=pc.card_id,
|
||||
quantity=pc.quantity,
|
||||
zone=pc.zone,
|
||||
try:
|
||||
new_deck = await _deck_mgr.clone_precedent(
|
||||
db=db,
|
||||
precedent_id=precedent_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
db.add(new_dc)
|
||||
|
||||
await db.flush()
|
||||
|
||||
return {
|
||||
"message": "Precedent cloned into new deck",
|
||||
"deck_id": new_deck.id,
|
||||
"deck_name": new_deck.name,
|
||||
"card_count": len(precedent_cards),
|
||||
}
|
||||
return {
|
||||
"message": "Precedent cloned into new deck",
|
||||
"deck_id": new_deck.id,
|
||||
"deck_name": new_deck.name,
|
||||
"card_count": len(await _deck_mgr.get_deck_cards(db=db, deck_id=new_deck.id)),
|
||||
}
|
||||
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 =====
|
||||
@@ -697,6 +635,8 @@ async def search_cards_for_deck(
|
||||
|
||||
|
||||
# ===== 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(
|
||||
|
||||
@@ -19,14 +19,23 @@ from app.models.user_data import (
|
||||
UserGroup, GroupMember, GroupChatMessage, UserNetwork,
|
||||
NetworkMember, UserPreference, UserActivityLog
|
||||
)
|
||||
from app.schemas.user_card_collection import (
|
||||
CardCollectionCreate,
|
||||
CardCollectionUpdate,
|
||||
CardCollectionResponse,
|
||||
CardCollectionListResponse,
|
||||
WishlistCreate,
|
||||
WishlistUpdate,
|
||||
WishlistResponse,
|
||||
WishlistListResponse,
|
||||
)
|
||||
from app.schemas.generic_schemas import MessageResponse, CountResponse, ErrorDetail
|
||||
from app.schemas.user_data_schemas import (
|
||||
SessionCleanupResponse,
|
||||
DeckVersionCreate, DeckVersionUpdate, DeckVersionResponse, DeckVersionListResponse,
|
||||
GameReplayCreate, GameReplayUpdate, GameReplayResponse, GameReplayListResponse,
|
||||
GameOutcomeCreate, GameOutcomeResponse, GameOutcomeListResponse,
|
||||
UserStatisticsResponse, StatisticsUpdateResponse,
|
||||
CardCollectionCreate, CardCollectionUpdate, CardCollectionResponse, CardCollectionListResponse,
|
||||
WishlistCreate, WishlistUpdate, WishlistResponse, WishlistListResponse,
|
||||
GroupCreate, GroupUpdate, GroupResponse, GroupListResponse,
|
||||
GroupMemberCreate, GroupMemberUpdate, GroupMemberRemove,
|
||||
GroupChatMessageCreate, GroupChatMessageResponse, GroupChatMessageListResponse,
|
||||
@@ -34,7 +43,6 @@ from app.schemas.user_data_schemas import (
|
||||
NetworkMemberCreate,
|
||||
UserPreferenceUpdate, UserPreferenceResponse,
|
||||
ActivityLogEntry, ActivityLogListResponse,
|
||||
MessageResponse, CountResponse, ErrorDetail
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
Reference in New Issue
Block a user