feat: add GET /api/v2/refractor/cards list endpoint (#172) #173
No reviewers
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#173
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue/172-feat-add-get-api-v2-refractor-cards-list-endpoint"
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?
Closes #172
Summary
Adds
GET /api/v2/refractor/cardsto the refractor router — the missing endpoint needed by the Discord bot's/refractor statuscommand.Endpoint behaviour
team_id(required): filters to a single team's refractor statescard_typefilter:"batter","sp","rp"viaRefractorTrack.card_typetierfilter:0–4viaRefractorCardState.current_tierseasonfilter: EXISTS subquery againstbatting_season_statsORpitching_season_stats— avoids row multiplication from players with both batting and pitching statsprogress=closefilter: CASE expression mapscurrent_tier→ next threshold column; returns only cards wherecurrent_value >= next_threshold * 0.8(and not fully evolved)limit(1–100, default 10) /offset(≥0, default 0) paginationcountreflects total matching rows before limit/offset for pagination UIcurrent_tier DESC, current_value DESCplayer_name: nullrather than erroringResponse shape additions
_build_card_state_response()extended with two new fields:progress_pct—round(current_value / next_threshold * 100, 1),nullwhen fully evolvedplayer_name— included when passed from list join; the single-card endpoint gainsprogress_pctautomatically,player_nameomitted until updated separatelyIndex
Non-unique
(team_id)index added torefractor_card_stateindb_engine.py(for SQLite dev) and a migration SQL file for production PostgreSQL.Files changed
app/routers_v2/refractor.py— newlist_card_statesendpoint; extended_build_card_state_response(); removed pre-existing unusedRefractorTrackimport inevaluate_game(ruff required)app/db_engine.py— non-unique(team_id)index onrefractor_card_statemigrations/2026-03-25_add_refractor_card_state_team_index.sql— production migrationOther observations
GET /api/v2/teams/{team_id}/refractorsinteams.py:1533is the partial predecessor to this endpoint. The spec recommends deprecating it in a future release (Phase 2: add deprecation log; Phase 3: remove). Not changed in this PR.GET /refractor/cards/{card_id}endpoint will now includeprogress_pctin responses (it calls_build_card_state_response()withoutplayer_name). This is a non-breaking addition.AI Code Review
Files Reviewed
app/routers_v2/refractor.py(modified — newlist_card_statesendpoint, extended_build_card_state_response, removed unused import)app/db_engine.py(modified — new team index onrefractor_card_state)migrations/2026-03-25_add_refractor_card_state_team_index.sql(added)Findings
Correctness
countfield: Correctly computed from un-limited query before.offset().limit()— consistent with PR #169's total-count behavior. ✅GET /cardsis inserted beforeGET /cards/{card_id}. No ambiguity — exact path vs path-param route; FastAPI handles these correctly. ✅progress=closeCASE expression: For tiers 0–3, the CASE maps to the correct threshold column; tier-4 (fully evolved) returns NULL, but thefully_evolved == Falseguard already excludes those rows before the comparison. Logically sound. ✅seasonfilter: Filters by bothplayerandteam— correct, since refractor state is keyed per (player, team) pair. Avoids row multiplication from two-way players. ✅RefractorTrackfromevaluate_gamelazy import: Correct — the class is never directly referenced by name in that function body;state.trackis a Peewee FK accessor. ✅fn,Case,JOINavailability:db_engine.pyusesfrom peewee import *, so all three are in scope for re-import. ✅BattingSeasonStats/PitchingSeasonStats: Both confirmed indb_engine.py(lines 1053, 1104). ✅BEGIN/COMMIT,CREATE INDEX IF NOT EXISTS(idempotent), and a rollback comment — follows the convention flagged in the PR #131 review. ✅Security
valid_token) applied before any DB access on the new endpoint — same pattern as all other endpoints. ✅Style & Conventions
player_nameretrieval viatry/except Exception: pass— consistent with the same pattern inevaluate_game(line 209–213). ✅# noqa: E712comment on thefully_evolved == Falsecomparison — correct, Peewee requires==for SQL expression building. ✅Suggestions
progressparameter accepts arbitrary strings silently. Any value other than'close'applies no filter and returns normally. Consistent with codebase pattern (no enum validation on string filters elsewhere), but an explicit400on unsupported values would make the contract clearer to callers.progress_pctcan exceed 100 ifcurrent_value > next_threshold(shouldn't happen in practice given how tier-ups work, but worth noting for UI consumers). Amin(progress_pct, 100.0)cap or a docstring note would future-proof it.season,progress=close, and pagination filter combinations would be valuable given the query complexity.Verdict: APPROVED ✅
Clean, correct implementation. The
count-before-limit behavior, EXISTS-subquery season filter, CASE-expression progress filter, and LEFT JOIN player name resolution are all well-reasoned. The migration follows project conventions. Suggestions above are non-blocking.Automated review by Claude PR Reviewer
Approved as cal — reviewer verdict is APPROVED with no blocking concerns. Non-blocking suggestions noted for follow-up.