Review: Public teams endpoint — GET /public/teams

review-429-2026-03-26 Review

review ready

Verdict: READY

Template Completeness

  • [x] Type — Feature
  • [x] Lineage — "Enables dynamic teams page on westside-app public site"
  • [x] Repo — forgejo_admin/basketball-api
  • [x] User Story — well-formed As a/I want/So that
  • [x] Context — explains why existing endpoints require auth, references playground data contract
  • [x] File Targets — new file public.py, modify main.py, schemas in route file
  • [x] Acceptance Criteria — 6 criteria, all testable
  • [x] Test Expectations — 4 test cases covering auth, allowlist, empty teams, sort
  • [x] Constraints — no auth, hardcode tenant_id=1, coach join documented
  • [x] Checklist — present
  • [x] Related — references playground, convention, westside-app #96
  • [x] Security — Field Allowlist — CRITICAL section with explicit allow/deny lists (above and beyond template requirements)

Traceability

  • [x] story:WS-S26 label — Westside Season 26 public site story
  • [x] arch:basketball-api label — correct architecture component
  • [x] Forgejo issue — forgejo_admin/basketball-api#176, open

File Targets

  • [x] src/basketball_api/routes/public.py — confirmed does NOT exist yet (new file, correct)
  • [x] src/basketball_api/main.py — confirmed exists, router registration pattern at lines 67-88 is clear; adding public_router with prefix="/public" follows established convention
  • [x] Schemas in route file (not shared models) — ticket explicitly says "New schemas in the route file (not in shared models)" which is correct for isolation
  • [x] Player model fields verified: name (L178), jersey_number (L213), position (L180), height (L179), current_school (L184) — all exist on the Player model
  • [x] Team model fields verified: id, name, coach_id FK, coach relationship, players M2M — all present
  • [x] Coach model verified: name field exists (L262), join via team.coach_id confirmed

Repo Placement

Correct. Issue filed on forgejo_admin/basketball-api, all file targets are in that repo. No cross-repo work required for this ticket.

Dependencies

  • Board item #430 (Public coaches endpoint — GET /public/coaches, Forgejo #177) is a sibling ticket also creating a route in public.py. Both tickets specify public.py as a new file. Whoever goes second will modify instead of create. This is fine — no conflict, but the agent should check if the file exists before creating it.
  • Board item #431 (SvelteKit public site — layout shell + 8 pages) has label depends:bb-176+bb-177, confirming this ticket is a prerequisite for the frontend. Dependency is correctly documented.
  • No blockers found. This ticket is not blocked by anything in in_progress.

Acceptance Criteria

All 6 acceptance criteria are verifiable by an agent:
  • "GET /public/teams returns teams with nested players, no auth required" — testable via unauthenticated HTTP call
  • "Response schema is PublicTeamsResponse — allowlisted fields only" — testable by asserting response keys match allowlist
  • "Teams sorted 17U to 16U to 15U, Elite to Select to Local" — testable by asserting order in response array
  • "Teams with zero assigned players are excluded" — testable with fixture data
  • "No sensitive data in response" — testable by asserting excluded keys are absent
  • "Endpoint accessible without Bearer token" — testable (same as criterion 1)
Test expectations match acceptance criteria well. The test command is implied (pytest) and follows the existing pattern in tests/test_teams.py.
Minor note: The sort order "17U, 16U, 15U then Elite, Select, Local" references team name conventions, not the AgeGroup enum (which has U8-U18 values like "U16", "U18"). The agent will need to implement custom sort logic based on age_group enum descending + team name substring for tier. This is implementation detail, not a scope gap — the ticket correctly specifies the desired output order.

Blast Radius

  • No existing /public routes exist in the codebase — this is greenfield. No collision risk.
  • Auth is per-route via Depends(), not global middleware — no risk of accidentally protecting or unprotecting other routes.
  • CORS middleware allows the westside frontend origin — no CORS issues for the public site consuming this endpoint.
  • The Division enum (boys/girls) is unrelated to the "Elite/Select/Local" sort tier. No enum changes needed.
  • No other services consume team data publicly — blast radius is contained to basketball-api + westside-app frontend.

Recommendation

No action needed. Scope is solid, all file targets verified, traceability complete, security allowlist is thorough, and acceptance criteria are testable. Ready for next_up.