feat: GET /api/jersey-public-orders admin endpoint (#432) #451
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api!451
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "432-jersey-public-get-admin"
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
Adds the admin-gated
GET /api/jersey-public-orderslist endpoint toroutes/jersey_public.py(T5 of the System B production rollout). Appends to the file created by T3 (#430/#450) without modifying the existing POST handler.The endpoint returns public jersey intake submissions with filters (status, from_date, to_date), offset pagination, and a 500-row hard cap — ready for the westside-landing admin review-and-reconcile table.
Closes #432
Changes
src/basketball_api/routes/jersey_public.pydate,Query,require_rolerequire_admin = require_role("admin")(mirrors the pattern fromroutes/subscriptions.py:19)_ALLOWED_STATUSESset constant for validationJerseyPublicOrderListItem,JerseyPublicOrderListResponseGET ""routelist_jersey_public_orders:status(query alias — internal paramstatus_filterto avoid shadowingfastapi.status)from_date,to_date,limit(default 100, cap 500),offset(default 0)totalbefore applying limit/offsetcreated_at DESC, id DESCstatusvaluesubmission_ipfrom response (internal triage data)tests/test_jersey_public.pyadmin_clientfixture that overridesrequire_admin(matchestests/test_admin.pypattern)_seed_orderhelper with directcreated_atoverride for stable ordering testsAuth chain
Per
feedback_funnel_requires_auth.md:Uses
require_admin = require_role("admin")frombasketball_api.auth, validates the Keycloak JWT against the westside-basketball realm and enforces theadminrole claim. Unauthenticated requests get 401 fromget_current_user; authenticated non-admins get 403 fromrequire_role. Matches the primitive atroutes/admin.py:48androutes/subscriptions.py:19.Test Plan
pytest tests/test_jersey_public.py -v— all 37 tests pass (24 T3 + 13 new T5)python -c "from basketball_api.routes.jersey_public import router; print([(r.path, sorted(list(r.methods))) for r in router.routes])"shows BOTH('', ['POST'])and('', ['GET'])git diff main -- routes/checkout.py routes/jersey.py models.py auth.py main.py— empty (hands-off files untouched)git diff main -- routes/jersey_public.py | grep "^-"— only the 3 import lines that were expanded (T3 POST handler byte-identical)ruff format --checkandruff check— cleanUnit test coverage:
{items: [], total: 0}created_at DESC, id DESCorder?status=pendingfilter?status=bogus→ 400?from_date=...&to_date=...range filter?limit=10caps rows?limit=1000accepted (internally clamped to 500)?offset=20&limit=10returns correct slicesubmitter_keycloak_subsubmission_iptotalignores limit/offset, respects filtersReview Checklist
git diff)schemas/directory created)require_adminreused fromrequire_role("admin")— no new auth logicField(..., pattern=...), noconstr(regex=...))ruff format+ruff checkcleanRelated Notes
story:WS-S31— admin public jersey intake linkarch-jersey-intakeroutes/admin.py:48,routes/subscriptions.py:19forrequire_adminprimitive patternAdd admin-gated list endpoint to routes/jersey_public.py with filters (status, from_date, to_date), offset pagination, and 500-row hard cap. Uses require_admin dependency from auth.require_role("admin"). Response excludes submission_ip (internal triage data). 13 new tests covering auth (401/403), empty DB, ordering, status filter, date filter, limit cap, offset pagination, submitter_keycloak_sub inclusion, and submission_ip exclusion. All 37 jersey_public tests pass. Closes #432PR #451 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Pydantic v2. T5 of System B production rollout — adds admin-gated
GET /api/jersey-public-ordersto the existingroutes/jersey_public.py.Auth gate (CRITICAL — verified clean):
require_admin = require_role("admin")declared at module scope, imported frombasketball_api.auth. Endpoint signature:user: User = Depends(require_admin). Correct — this is the admin-gated primitive, notget_current_user.require_roleinternally depends onget_current_user, so unauth → 401 (fromget_current_user), authed non-admin → 403 (fromrequire_role's role check). Auth chain matchesfeedback_funnel_requires_auth.md.routes/admin.py:48androutes/subscriptions.py:19. No new auth logic — zero DRY violation risk in the security path.Depends(require_role("admin"))) is the correct pattern fordependency_overridesto work in tests.T3 POST handler byte-identical (CRITICAL — verified clean):
The only
-lines in the diff are the three import-line expansions:from datetime import datetime→from datetime import date, datetimefrom fastapi import APIRouter, Depends, HTTPException, Request, status→…, Query, Request, statusfrom basketball_api.auth import User, get_current_user→…, require_rolePOST handler body (
create_jersey_public_order) is unchanged. Append-only patch, as promised.Query correctness:
limit = min(limit, 500)after negative clamp. Good defensive input handling (spec only required 500, dev went further).created_at DESC, id DESC— id tiebreaker gives stable pagination on same-millisecond inserts.total = query.count()computed AFTER status/date filters, BEFOREoffset/limit. Client-side pagination math works._ALLOWED_STATUSESset + 400 on invalid. SQL-injection safe because the query uses the validated literal.datetype parses ISOYYYY-MM-DD— good. (See nit below on datetime/date comparison semantics.)Response shape:
JerseyPublicOrderListItemincludessubmitter_keycloak_sub(admin cross-reference).submission_ipis NOT in the schema — correctly excluded from admin UI output per spec. Triage-only field stays server-side.constr/regex=. UsesOptional[...]for nullable fields.The
status_filtershadow workaround:status_filter: Optional[str] = Query(default=None, alias="status")— client uses?status=pending(verified by the filter test passing withparams={"status": "pending"}), internal name dodges thefastapi.statusmodule import. Clean, idiomatic handling of the naming collision.Test coverage:
All 13 required cases present and mapped:
get_current_userto exercise the realrequire_role403 path — correct){items: [], total: 0}?status=pendingfilter,?status=bogus→ 400?from_date=...&to_date=...range?limit=10caps rows,?limit=1000accepted without 422?offset=20&limit=10pagination slicesubmitter_keycloak_subpresent,submission_ipabsenttotalignores limit/offsetTests mock
require_adminviaapp.dependency_overrides[require_admin] = ...— no real Keycloak in CI. Matchestests/test_admin.pypattern.Hands-off files verified untouched:
Diff only modifies
src/basketball_api/routes/jersey_public.pyandtests/test_jersey_public.py.checkout.py,jersey.py,routes/__init__.py,models.py,main.py,auth.py,alembic/versions/*— all untouched.BLOCKERS
None.
NITS
Limit-clamp test only seeds 5 rows.
test_get_admin_limit_over_500_is_clampedproves the endpoint accepts?limit=1000without a 422 but cannot actually prove the 500-row cap because the dataset is tiny. Dev agent acknowledges this in the test comment. Stronger assertion would be a unit-level check of the internallimitvariable (e.g., via a parametrized test that monkeypatchesminor asserts row count when seeding 501). Non-blocking for T5 — functional behavior is still visible via code review.to_dateedge-case semantics.created_at <= to_datewithto_date: datewill coerce the date to midnight of that day in Postgres, meaning?to_date=2026-04-05excludes any row created after 00:00:00 on April 5. Admin users will reasonably expect "to April 5" to include all of April 5. Two options for follow-up: (a) document in docstring that the range is half-open[from_date 00:00, to_date 00:00), or (b) coerceto_date + timedelta(days=1)internally so the full day is included. Current tests happen to pass because they use dates outside the seeded range. File as follow-up ticket, not a blocker.Mid-file imports in the test file (
from datetime import datetime, timedelta, timezone # noqa: E402after line 214). Functional andnoqa'd, but moving to the top of the file is cleaner style. Minor ruff-hygiene nit.Loop-seeded tests commit per row.
_seed_ordercallsdb.commit()inside the helper; tests that seed 25–30 rows do 25–30 round-trips. Fine for test speed at this scale but worth noting if the suite grows.SOP COMPLIANCE
432-jersey-public-get-adminfollows{issue}-{kebab}conventionCloses #432presentWS-S31, archarch-jersey-intake, board item #950, dependencies #429 and #430/#450feedback_funnel_requires_auth.mdPROCESS OBSERVATIONS
require_admindeclaration is the right call: keeps the dependency overridable in tests and matches the establishedsubscriptions.py:19pattern. Consistency across admin endpoints reduces cognitive load for reviewers.status_filteralias workaround is the right solution to an annoying FastAPI-specific collision. Noting it in the PR body saves the next reviewer a trip through the code.to_datehalf-open-range semantics. Not urgent — the admin UI isn't wired up yet — but worth capturing before someone hits it in production.VERDICT: APPROVED