Review: T3 — basketball-api POST /api/jersey-public-orders
Verdict: NEEDS_REFINEMENT
Forgejo issue: basketball-api#430 — open. Board item 948. Reviewed 2026-04-10 by Dottie.
Template Completeness
- [x] Type — Feature
- [x] Lineage — references #429, arch-jersey-intake, feedback_funnel_requires_auth
- [x] Repo — forgejo_admin/basketball-api
- [x] User Story — parent/player via westside-basketball realm
- [x] Context — explains revision from public to auth
- [x] File Targets — present (issues flagged below)
- [x] Acceptance Criteria — 12 items
- [x] Test Expectations — 9 tests + run command
- [x] Constraints — documented
- [x] Checklist — present
- [x] Related — story, arch, deps
Traceability
- [x] story:WS-S31 — verified in project-westside-basketball user-stories (stories-admin-list): "As an admin, I want a long-standing public jersey order link..."
- [x] arch:jersey-intake label on board item
- [ ] arch note MISSING — [SCOPE] Create architecture note
arch-jersey-intake. search_notes returned empty. - [x] Forgejo issue #430 valid and open
- [ ] [SCOPE]
feedback_funnel_requires_authnote is cited as policy justification but search_notes returns empty. Either create the feedback note or correct the reference.
File Targets
- [x]
src/basketball_api/routes/jersey_public.py— new file, correct placement under routes/. - [x]
src/basketball_api/main.py— exists; router registration pattern confirmed at lines 17, 67-94. - [ ]
src/basketball_api/schemas/jersey_public.py— ISSUE: the directorysrc/basketball_api/schemas/does not exist. Repo convention (seeroutes/checkout.pylines 38-63) is inline Pydantic models per route module. Creating a new top-levelschemas/package breaks convention with no justification. [BODY] Drop the schemas file and declare models inline injersey_public.py. - [ ]
src/basketball_api/routes/__init__.py— ISSUE: existing__init__.pyis empty; routers are imported directly in main.py (e.g. line 17from basketball_api.routes.jersey import router as jersey_router). No export needed. [BODY] Drop the__init__.pymodification from File Targets. - CRITICAL
routes/checkout.pyauth-pattern reference — see Blast Radius.
Repo Placement
OK. Scoped to forgejo_admin/basketball-api only. System A (routes/jersey.py) and System C (routes/checkout.py) correctly marked hands-off.
Dependencies
- [ ] T2 / #429 — migration 014 for jersey_public_orders — CRITICAL: alembic slot 014 is already occupied by
014_add_password_reset_tokens.py. Repo is at 030 (030_add_registration_type_to_registrations.py). Next free slot is 031. The "014" label in T2 and T3 titles is factually wrong. This is primarily a T2 problem but T3's Lineage repeats the claim. [BODY] Update Lineage to reference the corrected migration number once T2 is refined; drop "014". - [x] T3 board item 948 (backlog), T2 board item 947 (backlog). Wave sequencing correct (T2 wave:1 → T3 wave:2).
Acceptance Criteria
Thorough coverage:
- Auth: 401 on missing header, 401 on invalid token — covered.
- Happy path: 201 with id/status/created_at — covered.
- submitter_keycloak_sub populated from JWT sub — covered.
- player_name/email NOT overridden from JWT — explicitly covered (important parent-for-child distinction).
- Field validation (kq, tier, preferred numbers, required fields) — covered.
- submission_ip from X-Forwarded-For first entry — covered.
- CORS for westsidekingsandqueens.tail5b443a.ts.net — covered.
Note on "401/403" from the reviewer brief: 403 is not applicable — the endpoint requires only authentication, not a role. 401-only is correct. No action needed.
Blast Radius
Code surface is small: routes/jersey.py (System A) and routes/checkout.py (System C) untouched. No shared Pydantic models to revise.
CRITICAL — the Keycloak dependency assumption is factually wrong. The ticket states "reuse existing
keycloak_user dependency from routes/checkout.py" and "find and reuse it, do NOT roll a new one." Verified in the repo:grep -rn "keycloak_user" src/basketball_api/returns ZERO hits. No such name exists.routes/checkout.pyuses onlyDepends(get_db)andDepends(require_admin)(line 265). It has no Keycloak user dep on its public endpoints — checkout is unauth or admin-only.- The actual JWT-validating dep is
get_current_userinsrc/basketball_api/auth.pylines 77-159. It returns aUserdataclass with fieldssub, email, username, roles, validates RS256 via JWKS from the westside-basketball realm, accepts internal and external issuer URLs, raises 401 on missing/invalid token. This IS the correct primitive —user.submaps directly tosubmitter_keycloak_sub. - Real reference files that use the pattern:
routes/account.py,routes/players.py,routes/subscriptions.py,routes/teams.py,routes/coaches_api.py,routes/tryouts.py,routes/upload.py.
An agent following the ticket literally will grep for
keycloak_user, fail, then either invent a new dep (violating "do NOT roll new auth") or open checkout.py, not find any auth, and improvise. The architectural intent is right; the file/symbol references are wrong.[BODY] Replace all references to
keycloak_user and routes/checkout.py (for auth pattern) with:- Dependency:
get_current_userfrombasketball_api.auth - Reference module:
routes/account.py(or any of the routes listed above) as the real pattern - Usage:
user: User = Depends(get_current_user), then persistuser.subassubmitter_keycloak_sub - Remove the "System C pattern" framing — checkout is not a Keycloak auth reference.
Main.py prefix nit: Ticket says "register with prefix
/api/jersey-public-orders". Repo convention (line 74, /api/subscriptions) typically uses the resource prefix on include_router and operation path on the route. Either works. Not blocking.Test mock wording: Test constraint says "mock JWT validation". More precise and idiomatic: use
app.dependency_overrides[get_current_user] to inject a fake User. [BODY] Reword the constraint accordingly.Decomposition Assessment
Three-thing limit: file targets after corrections = 2 new + 1 modify = 3 files, 1 repo. Within limit.
Five-minute rule: 12 AC but all clustered around a single endpoint + single DB insert + inline Pydantic models + 9 tests. Estimated 20-40 min for a well-scoped single-agent write. Slightly above 5 min but cohesive; decomposition would just add coordination overhead without reducing blast radius. No decomposition needed.
No independent parallelizable subtasks.
Recommendation
- [BODY] Replace
keycloak_userandroutes/checkout.pyauth-pattern references withget_current_userfrombasketball_api.auth. Point atroutes/account.pyas the reference. Remove "System C pattern" framing. - [BODY] Drop
src/basketball_api/schemas/jersey_public.pyfrom File Targets; declare Pydantic models inline injersey_public.py(matches checkout.py convention). - [BODY] Drop the
routes/__init__.pymodification — file is empty and unused. - [BODY] Update Lineage migration number from "014" to the correct next slot (031) once T2 is refined. Propagate to T2 review.
- [BODY] Reword test mock guidance to "use
app.dependency_overrides[get_current_user]to inject a fakeUser; do NOT hit real Keycloak or JWKS in CI." - [SCOPE] Create architecture note
arch-jersey-intakein pal-e-docs (referenced but missing). - [SCOPE] Create or verify the
feedback_funnel_requires_authnote — cited as policy justification but not found via search.
Architectural intent is sound. The problems are factual references, not design. Once the [BODY] fixes land and [SCOPE] items are resolved or explicitly deferred, T3 is ready for next_up (after T2 also passes review).
Re-Review Verdict: APPROVED
Re-reviewed 2026-04-10 by Dottie after ticket body was revised. Verified all [BODY] fixes landed in basketball-api#430 and confirmed one [SCOPE] item (arch-jersey-intake) now exists.
Fix Verification
- [x] Context documents verified auth primitive
get_current_userinsrc/basketball_api/auth.pylines 77-159 with User dataclass fields - [x] File Targets reduced to only
src/basketball_api/routes/jersey_public.py(new) andsrc/basketball_api/main.py(modify). No schemas/ file. No routes/__init__.py. Explicit 'do NOT touch' list added including routes/__init__.py - [x] Keycloak integration section uses
from basketball_api.auth import User, get_current_useranduser: User = Depends(get_current_user)verbatim - [x] Pydantic schemas section header states '(inline, in routes/jersey_public.py)' with full inline code block — no schemas/ directory referenced
- [x] Context explicitly states '
routes/checkout.pyis NOT a valid reference for Keycloak user auth — it only uses get_db and require_admin' - [x] Lineage references migration 031. Context states 'Migration head is 030... #429 creates migration 031'. No '014' anywhere in the body
- [x] Reference module is
routes/subscriptions.pyline 367 (cited in both Context and Related sections). Not checkout.py for auth - [x]
arch-jersey-intakeexists in pal-e-docs (note id 1406, updated 2026-04-10). Earlier semantic_search false negative — note is present and covers all three systems plus the System B Keycloak-gated flow
Residual Notes
- Constraints still cite 'Match inline-Pydantic convention from
routes/subscriptions.pyandroutes/checkout.py'. This is accurate — checkout.py does declare Pydantic models inline, which is distinct from the (now-corrected) auth-pattern claim. Not blocking. - Pydantic code example uses
constr(regex=...)which is Pydantic v1 syntax. If basketball-api is on Pydantic v2, the implementing agent will needconstr(pattern=...). Not a scope-review blocker — agent can reconcile against installed version. feedback_funnel_requires_authstill cited in Lineage; verification of that note was not part of this re-review brief. Flagging as carry-over [SCOPE] from the first pass.
Verdict: APPROVED. All seven required [BODY] fixes from the first-pass review landed cleanly. arch-jersey-intake exists and covers System B. T3/board-item 948 is ready to move from backlog to todo (after T2/#429 also passes review, since T3 depends on migration 031 from T2).