feat: add evaluated_only filter to GET /api/v2/refractor/cards (#174) #175
@ -107,6 +107,7 @@ async def list_card_states(
|
||||
tier: Optional[int] = Query(default=None, ge=0, le=4),
|
||||
season: Optional[int] = Query(default=None),
|
||||
progress: Optional[str] = Query(default=None),
|
||||
evaluated_only: bool = Query(default=True),
|
||||
limit: int = Query(default=10, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
token: str = Depends(oauth2_scheme),
|
||||
@ -114,15 +115,18 @@ async def list_card_states(
|
||||
"""List RefractorCardState rows for a team, with optional filters and pagination.
|
||||
|
||||
Required:
|
||||
team_id -- filter to this team's cards; returns empty list if team has no states
|
||||
team_id -- filter to this team's cards; returns empty list if team has no states
|
||||
|
||||
Optional filters:
|
||||
card_type -- one of 'batter', 'sp', 'rp'; filters by RefractorTrack.card_type
|
||||
tier -- filter by current_tier (0-4)
|
||||
season -- filter to players who have batting or pitching season stats in that
|
||||
season (EXISTS subquery against batting/pitching_season_stats)
|
||||
progress -- 'close' = only cards within 80% of their next tier threshold;
|
||||
fully evolved cards are always excluded from this filter
|
||||
card_type -- one of 'batter', 'sp', 'rp'; filters by RefractorTrack.card_type
|
||||
tier -- filter by current_tier (0-4)
|
||||
season -- filter to players who have batting or pitching season stats in that
|
||||
season (EXISTS subquery against batting/pitching_season_stats)
|
||||
progress -- 'close' = only cards within 80% of their next tier threshold;
|
||||
fully evolved cards are always excluded from this filter
|
||||
evaluated_only -- default True; when True, excludes cards where last_evaluated_at
|
||||
is NULL (cards created but never run through the evaluator).
|
||||
Set to False to include all rows, including zero-value placeholders.
|
||||
|
||||
Pagination:
|
||||
limit -- page size (1-100, default 10)
|
||||
@ -199,6 +203,9 @@ async def list_card_states(
|
||||
& (RefractorCardState.current_value >= next_threshold_expr * 0.8)
|
||||
)
|
||||
|
||||
if evaluated_only:
|
||||
query = query.where(RefractorCardState.last_evaluated_at.is_null(False))
|
||||
|
||||
total = query.count()
|
||||
items = []
|
||||
for state in query.offset(offset).limit(limit):
|
||||
|
||||
@ -956,3 +956,83 @@ def test_get_card_state_last_evaluated_at_null(setup_state_api_db, state_api_cli
|
||||
f"Expected last_evaluated_at=null for un-evaluated card, "
|
||||
f"got {data['last_evaluated_at']!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T3-8: GET /refractor/cards?team_id=X&evaluated_only=false includes un-evaluated
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_list_cards_evaluated_only_false_includes_unevaluated(
|
||||
setup_state_api_db, state_api_client
|
||||
):
|
||||
"""GET /refractor/cards?team_id=X&evaluated_only=false returns cards with last_evaluated_at=NULL.
|
||||
|
||||
What: Create two RefractorCardState rows for the same team — one with
|
||||
last_evaluated_at=None (never evaluated) and one with last_evaluated_at set
|
||||
to a timestamp (has been evaluated). Call the endpoint twice:
|
||||
1. evaluated_only=true (default) — only the evaluated card appears.
|
||||
2. evaluated_only=false — both cards appear.
|
||||
|
||||
Why: The default evaluated_only=True filter uses
|
||||
`last_evaluated_at IS NOT NULL` to exclude placeholder rows created at
|
||||
pack-open time but never run through the evaluator. At team scale (2753
|
||||
rows, ~14 evaluated) this filter is critical for bot performance.
|
||||
This test verifies the opt-out path (evaluated_only=false) exposes all rows,
|
||||
which is needed for admin/pipeline use cases.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
team = _sa_make_team("SA_T38", gmid=30380)
|
||||
track = _sa_make_track("batter")
|
||||
|
||||
# Card 1: never evaluated — last_evaluated_at is NULL
|
||||
player_unevaluated = _sa_make_player("T38 Unevaluated", pos="1B")
|
||||
card_unevaluated = _sa_make_card(player_unevaluated, team) # noqa: F841
|
||||
RefractorCardState.create(
|
||||
player=player_unevaluated,
|
||||
team=team,
|
||||
track=track,
|
||||
current_tier=0,
|
||||
current_value=0.0,
|
||||
fully_evolved=False,
|
||||
last_evaluated_at=None,
|
||||
)
|
||||
|
||||
# Card 2: has been evaluated — last_evaluated_at is a timestamp
|
||||
player_evaluated = _sa_make_player("T38 Evaluated", pos="RF")
|
||||
card_evaluated = _sa_make_card(player_evaluated, team) # noqa: F841
|
||||
RefractorCardState.create(
|
||||
player=player_evaluated,
|
||||
team=team,
|
||||
track=track,
|
||||
current_tier=1,
|
||||
current_value=5.0,
|
||||
fully_evolved=False,
|
||||
last_evaluated_at=datetime(2025, 4, 1, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Default (evaluated_only=true) — only the evaluated card should appear
|
||||
resp_default = state_api_client.get(
|
||||
f"/api/v2/refractor/cards?team_id={team.id}", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp_default.status_code == 200
|
||||
data_default = resp_default.json()
|
||||
assert data_default["count"] == 1, (
|
||||
f"evaluated_only=true should return 1 card, got {data_default['count']}"
|
||||
)
|
||||
assert data_default["items"][0]["player_name"] == "T38 Evaluated"
|
||||
|
||||
# evaluated_only=false — both cards should appear
|
||||
resp_all = state_api_client.get(
|
||||
f"/api/v2/refractor/cards?team_id={team.id}&evaluated_only=false",
|
||||
headers=AUTH_HEADER,
|
||||
)
|
||||
assert resp_all.status_code == 200
|
||||
data_all = resp_all.json()
|
||||
assert data_all["count"] == 2, (
|
||||
f"evaluated_only=false should return 2 cards, got {data_all['count']}"
|
||||
)
|
||||
names = {item["player_name"] for item in data_all["items"]}
|
||||
assert "T38 Unevaluated" in names
|
||||
assert "T38 Evaluated" in names
|
||||
|
||||
Loading…
Reference in New Issue
Block a user