GET /api/jersey-public-orders — admin list endpoint #432

Closed
opened 2026-04-10 21:53:06 +00:00 by forgejo_admin · 1 comment

Type

Feature

Lineage

Depends on basketball-api#429 (migration 031) AND basketball-api#430 (POST endpoint creates routes/jersey_public.py). Part of System B production rollout. Architecture in arch-jersey-intake. Revised 2026-04-10 (three times): migration number corrected to 031; dropped non-existent schemas/ target; added explicit limit cap AC; added dependency on #430.

Repo

forgejo_admin/basketball-api

User Story

As Marcus (admin)
I want an authenticated GET endpoint listing public jersey submissions with filters
So that the westside-landing admin UI can render a review-and-reconcile table

Context

Verified 2026-04-10 against live basketball-api source:

  • Admin-gate primitive: require_admin = require_role("admin") defined at routes/admin.py line 48 (also mirrored at routes/subscriptions.py line 19). Used as user: User = Depends(require_admin).
  • Pydantic schemas are inline in route files — no schemas/ directory exists.
  • This ticket modifies routes/jersey_public.py which is created by #430, so #430 must merge first (in addition to #429 for the model).

File Targets

Files to modify:

  • src/basketball_api/routes/jersey_public.py — add GET /api/jersey-public-orders route (admin-gated). Append the new response schema inline at the top of the file alongside the existing JerseyPublicOrderIn / JerseyPublicOrderCreated from #430.

Files the agent should NOT touch:

  • routes/admin.py — reference only for the require_admin primitive
  • routes/subscriptions.py — reference only for usage pattern
  • routes/checkout.py — System C, hands off
  • src/basketball_api/models.py — already populated by #429
  • Anything outside routes/jersey_public.py

Endpoint spec

GET /api/jersey-public-orders?status=pending&from_date=2026-04-01&to_date=2026-04-30&limit=100&offset=0
Authorization: Bearer <JWT with realm_access.roles containing 'admin'>

Query params:
- status    (optional): 'pending' | 'reviewed' | 'fulfilled' | 'cancelled'
- from_date (optional): ISO date YYYY-MM-DD, filters created_at >= date
- to_date   (optional): ISO date, filters created_at <= date
- limit     (optional, default 100, HARD CAP 500 — requests with limit > 500 are clamped to 500)
- offset    (optional, default 0)

Success (200):
{
  "items": [ JerseyPublicOrderListItem[] ordered by created_at DESC, id DESC ],
  "total": 42
}

Errors:
- 401: missing/invalid token (from get_current_user dep via require_admin)
- 403: token valid but user lacks 'admin' role (from require_admin)
- 400: invalid status value or malformed date

Each item includes: id, submitter_keycloak_sub, player_name, email, team, kq, preferred_number_1/2/3, top_size, short_size, tier, status, linked_parent_id, linked_player_id, reviewed_at, reviewed_by, created_at. Do NOT include submission_ip — that's internal triage data, not admin UI data.

Pydantic schema (inline addition to routes/jersey_public.py)

class JerseyPublicOrderListItem(BaseModel):
    id: UUID
    submitter_keycloak_sub: str
    player_name: str
    email: str
    team: str
    kq: str
    preferred_number_1: Optional[str]
    preferred_number_2: Optional[str]
    preferred_number_3: Optional[str]
    top_size: str
    short_size: str
    tier: str
    status: str
    linked_parent_id: Optional[int]
    linked_player_id: Optional[int]
    reviewed_at: Optional[datetime]
    reviewed_by: Optional[str]
    created_at: datetime

class JerseyPublicOrderListResponse(BaseModel):
    items: list[JerseyPublicOrderListItem]
    total: int

Keycloak integration

  • Import: from basketball_api.auth import User, require_role (or reuse if already imported by #430)
  • Define require_admin = require_role("admin") at module scope if not already present
  • Route signature: async def list_jersey_public_orders(..., user: User = Depends(require_admin), db: Session = Depends(get_db))

Acceptance Criteria

  • Unauthenticated → 401
  • Non-admin token → 403
  • Admin token → 200 with items ordered by created_at DESC, id DESC
  • ?status=pending filters correctly
  • ?from_date=...&to_date=... date range works
  • ?limit=10 caps rows to 10
  • ?limit=1000 is clamped to 500 (hard cap)
  • ?offset=20&limit=10 returns rows 21–30
  • total respects status/date filters but NOT limit/offset
  • Invalid status value → 400
  • Malformed from_date / to_date → 400
  • Response includes submitter_keycloak_sub for every item
  • Response does NOT include submission_ip

Test Expectations

  • Unit test: unauth → 401
  • Unit test: non-admin → 403
  • Unit test: admin + empty DB → 200 with items=[]
  • Unit test: admin + seeded DB → 200 with stable order
  • Unit test: status filter
  • Unit test: date filter
  • Unit test: pagination (limit + offset)
  • Unit test: limit cap (1000 → 500)
  • Unit test: response shape omits submission_ip
  • Run command: pytest tests/ -k "jersey_public and admin"

Constraints

  • Reuse require_admin — do NOT roll new auth
  • Inline Pydantic schemas — no schemas/ directory
  • Stable ordering: created_at DESC, id DESC
  • Simple offset pagination (no cursor)
  • No write endpoints in this ticket (PATCH/PUT are separate)
  • Do NOT modify the POST endpoint from #430

Checklist

  • PR opened against basketball-api main
  • Depends on #429 (migration 031) AND #430 (POST endpoint) merged first
  • Tests pass
  • Admin gate verified via integration test
  • westside-basketball — project
  • story:WS-S31 — admin public jersey intake link
  • arch-jersey-intake — architecture doc
  • Depends on: basketball-api#429 (migration 031), basketball-api#430 (creates jersey_public.py)
  • Reference: routes/admin.py line 48 for require_admin primitive
  • Reference: routes/subscriptions.py for inline-pydantic + require_admin usage pattern
### Type Feature ### Lineage Depends on `basketball-api#429` (migration 031) AND `basketball-api#430` (POST endpoint creates `routes/jersey_public.py`). Part of System B production rollout. Architecture in `arch-jersey-intake`. **Revised 2026-04-10 (three times):** migration number corrected to 031; dropped non-existent `schemas/` target; added explicit `limit` cap AC; added dependency on #430. ### Repo `forgejo_admin/basketball-api` ### User Story As Marcus (admin) I want an authenticated GET endpoint listing public jersey submissions with filters So that the westside-landing admin UI can render a review-and-reconcile table ### Context **Verified 2026-04-10 against live basketball-api source:** - Admin-gate primitive: `require_admin = require_role("admin")` defined at `routes/admin.py` line 48 (also mirrored at `routes/subscriptions.py` line 19). Used as `user: User = Depends(require_admin)`. - Pydantic schemas are **inline** in route files — no `schemas/` directory exists. - This ticket **modifies** `routes/jersey_public.py` which is created by #430, so #430 must merge first (in addition to #429 for the model). ### File Targets Files to modify: - `src/basketball_api/routes/jersey_public.py` — add `GET /api/jersey-public-orders` route (admin-gated). Append the new response schema **inline** at the top of the file alongside the existing `JerseyPublicOrderIn` / `JerseyPublicOrderCreated` from #430. Files the agent should NOT touch: - `routes/admin.py` — reference only for the `require_admin` primitive - `routes/subscriptions.py` — reference only for usage pattern - `routes/checkout.py` — System C, hands off - `src/basketball_api/models.py` — already populated by #429 - Anything outside `routes/jersey_public.py` ### Endpoint spec ``` GET /api/jersey-public-orders?status=pending&from_date=2026-04-01&to_date=2026-04-30&limit=100&offset=0 Authorization: Bearer <JWT with realm_access.roles containing 'admin'> Query params: - status (optional): 'pending' | 'reviewed' | 'fulfilled' | 'cancelled' - from_date (optional): ISO date YYYY-MM-DD, filters created_at >= date - to_date (optional): ISO date, filters created_at <= date - limit (optional, default 100, HARD CAP 500 — requests with limit > 500 are clamped to 500) - offset (optional, default 0) Success (200): { "items": [ JerseyPublicOrderListItem[] ordered by created_at DESC, id DESC ], "total": 42 } Errors: - 401: missing/invalid token (from get_current_user dep via require_admin) - 403: token valid but user lacks 'admin' role (from require_admin) - 400: invalid status value or malformed date ``` Each item includes: id, submitter_keycloak_sub, player_name, email, team, kq, preferred_number_1/2/3, top_size, short_size, tier, status, linked_parent_id, linked_player_id, reviewed_at, reviewed_by, created_at. Do NOT include `submission_ip` — that's internal triage data, not admin UI data. ### Pydantic schema (inline addition to routes/jersey_public.py) ```python class JerseyPublicOrderListItem(BaseModel): id: UUID submitter_keycloak_sub: str player_name: str email: str team: str kq: str preferred_number_1: Optional[str] preferred_number_2: Optional[str] preferred_number_3: Optional[str] top_size: str short_size: str tier: str status: str linked_parent_id: Optional[int] linked_player_id: Optional[int] reviewed_at: Optional[datetime] reviewed_by: Optional[str] created_at: datetime class JerseyPublicOrderListResponse(BaseModel): items: list[JerseyPublicOrderListItem] total: int ``` ### Keycloak integration - Import: `from basketball_api.auth import User, require_role` (or reuse if already imported by #430) - Define `require_admin = require_role("admin")` at module scope if not already present - Route signature: `async def list_jersey_public_orders(..., user: User = Depends(require_admin), db: Session = Depends(get_db))` ### Acceptance Criteria - [ ] Unauthenticated → 401 - [ ] Non-admin token → 403 - [ ] Admin token → 200 with items ordered by `created_at DESC, id DESC` - [ ] `?status=pending` filters correctly - [ ] `?from_date=...&to_date=...` date range works - [ ] `?limit=10` caps rows to 10 - [ ] `?limit=1000` is clamped to 500 (hard cap) - [ ] `?offset=20&limit=10` returns rows 21–30 - [ ] `total` respects status/date filters but NOT limit/offset - [ ] Invalid `status` value → 400 - [ ] Malformed `from_date` / `to_date` → 400 - [ ] Response includes `submitter_keycloak_sub` for every item - [ ] Response does NOT include `submission_ip` ### Test Expectations - [ ] Unit test: unauth → 401 - [ ] Unit test: non-admin → 403 - [ ] Unit test: admin + empty DB → 200 with items=[] - [ ] Unit test: admin + seeded DB → 200 with stable order - [ ] Unit test: status filter - [ ] Unit test: date filter - [ ] Unit test: pagination (limit + offset) - [ ] Unit test: limit cap (1000 → 500) - [ ] Unit test: response shape omits submission_ip - [ ] Run command: `pytest tests/ -k "jersey_public and admin"` ### Constraints - Reuse `require_admin` — do NOT roll new auth - Inline Pydantic schemas — no `schemas/` directory - Stable ordering: `created_at DESC, id DESC` - Simple offset pagination (no cursor) - No write endpoints in this ticket (PATCH/PUT are separate) - Do NOT modify the POST endpoint from #430 ### Checklist - [ ] PR opened against `basketball-api` main - [ ] Depends on #429 (migration 031) AND #430 (POST endpoint) merged first - [ ] Tests pass - [ ] Admin gate verified via integration test ### Related - `westside-basketball` — project - `story:WS-S31` — admin public jersey intake link - `arch-jersey-intake` — architecture doc - Depends on: `basketball-api#429` (migration 031), `basketball-api#430` (creates jersey_public.py) - Reference: `routes/admin.py` line 48 for `require_admin` primitive - Reference: `routes/subscriptions.py` for inline-pydantic + require_admin usage pattern
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-950-2026-04-10

Scope is mostly solid — admin-gate pattern verified in routes/admin.py (require_admin = require_role("admin")), endpoint spec is complete, 11 AC are testable, no decomposition needed. Two fixable issues and one cascade risk:

[BODY] Fixes required before advancing to next_up:

  • Remove src/basketball_api/schemas/jersey_public.py from File Targets. basketball-api has no schemas/ directory — pydantic models live inline in route files (see routes/jersey.py). Define JerseyPublicOrderListItem inline in routes/jersey_public.py.
  • Add #430 (POST endpoint) to Lineage. T5 "modifies" routes/jersey_public.py, but that file is created by #430, not #429. Current Lineage only lists #429.
  • Add acceptance criterion for limit max cap (spec says max 500 but no AC covers limit=1000 behavior).

[SCOPE] Cascade risk on dependency:

  • #429 title claims "Migration 014" but alembic/versions/014_add_password_reset_tokens.py is already merged. Alembic head is 019. #429 must be renumbered to 020+ before it lands, or T5 cannot run. Escalating to Ava.

Story WS-S31 existence on project-westside-basketball user-stories section was not verified — caller should confirm.

## Scope Review: NEEDS_REFINEMENT Review note: `review-950-2026-04-10` Scope is mostly solid — admin-gate pattern verified in `routes/admin.py` (`require_admin = require_role("admin")`), endpoint spec is complete, 11 AC are testable, no decomposition needed. Two fixable issues and one cascade risk: **[BODY] Fixes required before advancing to next_up:** - Remove `src/basketball_api/schemas/jersey_public.py` from File Targets. basketball-api has **no `schemas/` directory** — pydantic models live inline in route files (see `routes/jersey.py`). Define `JerseyPublicOrderListItem` inline in `routes/jersey_public.py`. - Add `#430` (POST endpoint) to Lineage. T5 "modifies" `routes/jersey_public.py`, but that file is created by #430, not #429. Current Lineage only lists #429. - Add acceptance criterion for `limit` max cap (spec says max 500 but no AC covers `limit=1000` behavior). **[SCOPE] Cascade risk on dependency:** - `#429` title claims "Migration 014" but `alembic/versions/014_add_password_reset_tokens.py` is already merged. Alembic head is 019. `#429` must be renumbered to 020+ before it lands, or T5 cannot run. Escalating to Ava. Story WS-S31 existence on `project-westside-basketball` user-stories section was not verified — caller should confirm.
forgejo_admin 2026-04-11 21:00:59 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#432
No description provided.