feat: add GET /api/v2/refractor/cards list endpoint #172
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#172
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The Discord bot's
/refractor statuscommand callsGET /api/v2/refractor/cards?team_id=Xto fetch all refractor card states for a team. This endpoint does not exist — only the single-cardGET /refractor/cards/{card_id}does. Without it,/refractor statusreturns "No refractor data found" for all users.A partial version exists at
GET /api/v2/teams/{team_id}/refractors(teams.py:1533) but it lacks the filters the bot needs and lives in the wrong router.Endpoint Spec
Route:
GET /api/v2/refractor/cardsQuery Parameters:
team_idcard_type"batter","sp", or"rp"tierseasonprogress"close"= within 80% of next thresholdlimitoffsetResponse:
{"count": N, "items": [...]}Each item includes:
player_id,player_name,team_id,current_tier,current_value,fully_evolved,last_evaluated_at,next_threshold,progress_pct,track(nested object with thresholds).Sorting:
current_tier DESC, current_value DESCKey design decisions
player_nameincluded in response to eliminate N+1 lookups in the botprogress_pctpre-computed server-side for consistencyseasonfilter uses EXISTS subquery against batting/pitching_season_statsprogress=closeuses CASE expression mapping tier to appropriate threshold columncount: 0, not 404(team_id)index onrefractor_card_stateFull spec
See
database/.claude/specs/refractor-cards-list-endpoint.mdBlocker for
/refractor statuscommand (all filters, pagination)Migration note
Once this ships, deprecate
GET /api/v2/teams/{team_id}/refractorsin favor of this endpoint.Full endpoint spec (click to expand)
Spec:
GET /api/v2/refractor/cards— List Refractor Card StatesStatus: Draft
Date: 2026-03-25
Author: pd-database (design advisor)
Background
The Discord bot's
/refractor statuscommand needs to display a paginated list of all cards on a team with their refractor progress. Currently, a similar endpoint exists atGET /api/v2/teams/{team_id}/refractors(inteams.pyline 1533), but it is nested under the teams router and lacks several filters the bot requires (season, progress proximity). This spec defines a canonical endpoint on the refractor router that supersedes the teams-nested version.Existing State
GET /api/v2/refractor/cards/{card_id}inrouters_v2/refractor.pyGET /api/v2/teams/{team_id}/refractorsinrouters_v2/teams.py:1533_build_card_state_response()inrouters_v2/refractor.py:24db_engine.py:1227— FKs to Player, Team, RefractorTrackdb_engine.py:1213— name, card_type, formula, t1-t4 thresholds(player, team)on refractor_card_stateRoute
Lives in
routers_v2/refractor.pyalongside the existing single-card endpoint.Query Parameters
team_idintcard_typestrNone"batter","sp","rp"RefractorTrack.card_typetierintNone0 <= tier <= 4RefractorCardState.current_tierseasonintNoneprogressstrNone"close"limitint101 <= limit <= 100offsetint0>= 0Design Decision:
limit/offsetvspage/per_pageThe existing teams-nested endpoint uses
page/per_page. This spec useslimit/offsetfor the new endpoint because:limit/offsetis the more standard REST convention and what the Discord bot client library expects.limit/offsetcomposes better with cursor-based pagination if we migrate later.Response Shape
Field Definitions
countintitems[].player_idintPlayer.player_idFKitems[].player_namestrPlayer.p_name— new field, not present in existing responseitems[].team_idintTeam.idFKitems[].current_tierintitems[].current_valuefloatitems[].fully_evolvedboolitems[].last_evaluated_atstrornullitems[].next_thresholdintornullnullwhen fully evolveditems[].progress_pctfloatornull(current_value / next_threshold) * 100, rounded to 1 decimal;nullwhen fully evolveditems[].trackobjectChanges from
_build_card_state_response()Two fields are added to the existing response shape:
player_name— the bot currently has to make N+1 calls to resolve player names for display. Including the name in the list response eliminates this.progress_pct— pre-computed percentage toward next threshold. The bot currently calculates this client-side; moving it server-side ensures consistency and simplifies theprogress=closefilter.The existing single-card endpoint (
GET /refractor/cards/{card_id}) should also be updated to include these two fields for consistency, but that is a separate, non-blocking change.ORM Query Strategy
Base Query
The Player join is required for
player_name. Using.switch()to return to RefractorCardState before joining Player (Peewee multi-join pattern).Filter:
card_typeFilter:
tierFilter:
seasonThis is the most complex filter.
RefractorCardStatedoes not store a season — it tracks career-aggregate progress. Theseasonfilter means: "only return states for players who have stats in this season" (i.e., players who were active in the given season).Approach — EXISTS subquery:
This avoids duplicating rows or needing DISTINCT. The EXISTS subqueries will use the existing composite indexes on
(player, team, season)that both season_stats tables already have.Filter:
progressWhen
progress=close, exclude fully evolved cards and filter to rows wherecurrent_value / next_threshold >= 0.8. Since "next threshold" depends on the current tier, this requires a CASE expression:Pagination
countis returned in the response for the bot to render page indicators.Sorting
Default sort:
current_tier DESC, current_value DESC.This puts the most progressed cards first, which is the natural display order for a "refractor status" view. Fully evolved (tier 4) cards appear at the top, followed by cards closest to their next tier-up.
Authentication
Same pattern as all other v2 endpoints:
Error Responses
{"detail": "Unauthorized"}tier=5,limit=0,card_type=invalid){"count": 0, "items": []}— this is NOT an errorDesign Decision: No 404 for empty results
An empty result set for a valid
team_idis normal (team exists but has no refractor-eligible cards yet). Returning 200 withcount: 0is consistent with howGET /api/v2/refractor/trackshandles no results.An invalid
team_id(team does not exist in the DB) also returns{"count": 0, "items": []}because the query simply finds no matching rows. We do not validate team existence separately — this avoids an extra query.Edge Cases
{"count": 0, "items": []}progress=closewith all cards fully evolvedseasonfilter with no matching statscard_typecombined withtierplayer_name: null.Index Considerations
The query filters on
RefractorCardState.teamas its primary discriminator. Current indexes:(player, team)— this helps for the team filter but is not optimally ordered (team is second in the composite).(team)alone.Migration Plan: Deprecate
GET /api/v2/teams/{team_id}/refractorsThe existing endpoint at
teams.py:1533overlaps significantly. After the new endpoint ships:GET /api/v2/refractor/cards. Update Discord bot to call it.Summary of Files to Change
app/routers_v2/refractor.pyGET /cardslist endpoint; extend_build_card_state_response()withplayer_nameandprogress_pctapp/db_engine.py(team_id)index onrefractor_card_stateapp/routers_v2/teams.pyPR #173 opens the implementation: #173
Approach:
GET /api/v2/refractor/cardson the refractor router with all filters from the spec (team_id,card_type,tier,season,progress,limit,offset)seasonfilter uses EXISTS subqueries againstbatting_season_statsandpitching_season_statsto avoid duplicates from players with both typesprogress=closeuses a PeeweeCaseexpression to mapcurrent_tier→ next threshold column, then filterscurrent_value >= next_threshold * 0.8player_name: null_build_card_state_response()extended withprogress_pct(always computed) and optionalplayer_name; single-card endpoint gainsprogress_pctas a non-breaking addition2026-03-25_add_refractor_card_state_team_index.sql