Review: Wire tournament checkout URLs into blast query

review-998-2026-04-12 Review

review ready

Verdict: READY

Template Completeness

  • [x] Type -- Feature
  • [x] Lineage -- references #456 and #457 as upstream
  • [x] Repo -- forgejo_admin/basketball-api
  • [x] User Story -- admin wants per-player Stripe payment links in tournament emails
  • [x] Context -- explains the gap between blast system and checkout sessions
  • [x] File Targets -- 5 files to modify, 3 files NOT to touch
  • [x] Acceptance Criteria -- 5 criteria, all verifiable
  • [x] Test Expectations -- 4 test types + run command
  • [x] Constraints -- migration slot, backfill, pattern adherence
  • [x] Checklist -- present
  • [x] Related -- references project, story, upstream issues, SOP

Traceability

  • [x] story:WS-S33 label -- "As an admin, I want to bill players for tournament entry fees at team-specific rates via Stripe so that event costs are collected before registration deadlines" -- verified in project-westside-basketball user-stories section
  • [x] story note verified -- found in project-westside-basketball Admin stories list
  • [x] arch:email label -- present on board item
  • [ ] arch:email note MISSING -- [SCOPE] No arch-email note found in pal-e-docs. Create architecture note arch-email for the email subsystem.
  • [x] arch:checkout label -- present on board item
  • [ ] arch:checkout note MISSING -- [SCOPE] No arch-checkout note found in pal-e-docs. Create architecture note arch-checkout for the checkout/payment subsystem.
  • [x] Forgejo issue -- #463, open

File Targets

  • [x] src/basketball_api/models.py -- verified: Order model at line 437, no stripe_checkout_url column exists yet. stripe_checkout_session_id (line 450) exists but not the URL. Ticket correctly identifies the gap.
  • [x] alembic/versions/NNN_add_stripe_checkout_url_to_orders.py -- verified: latest migration is 046_add_tournament_fee_email_types.py, so next slot is 047. Ticket says "currently 046" which is accurate.
  • [x] src/basketball_api/services/email_queries.py -- verified: query_tournament_committed at line 159, returns checkout_url: "" at line 222 with comment "populated by body.data override." Ticket's analysis is accurate -- body.data is shared across all recipients and cannot provide per-player URLs.
  • [x] src/basketball_api/routes/admin.py -- verified: POST /admin/tournaments/{id}/checkout-links at line 2227, calls create_player_checkout_session at line 2297, returns checkout_url but does NOT save it to the Order record.
  • [x] src/basketball_api/routes/checkout.py -- verified: create_player_checkout_session at line 467, saves stripe_checkout_session_id (line 555) but NOT the session URL. Returns (order, checkout_session.url) at line 567.

Repo Placement

Correct. All changes are within basketball-api. No cross-repo work needed. The blast endpoint, email queries, checkout logic, and Order model all live in this repo.

Dependencies

  • #456 (blast system) -- CLOSED/merged. Upstream dependency satisfied.
  • #457 (tournament checkout) -- CLOSED/merged. Upstream dependency satisfied.
  • No blocking items found in in_progress or next_up columns on board-westside-basketball.
  • Board item #434 (Fix 2 pre-existing failing tests on main) in backlog could affect test reliability but is not a hard blocker for this ticket.

Acceptance Criteria

All 5 criteria are verifiable by an agent:
  • AC1: Column population -- grep Order model after migration, check admin.py saves URL.
  • AC2: Query accepts tournament_id and returns checkout_url -- unit testable.
  • AC3: End-to-end flow -- integration testable with mocked Stripe.
  • AC4: Graceful fallback -- unit testable (player with no order returns empty string).
  • AC5: Backfill -- the constraint mentions a backfill script/migration step. This is testable.

Blast Radius

  • checkout_url is used in 6+ locations across email.py, checkout.py, jersey.py, and admin.py. The new column adds a storage mechanism but does not change existing consumers -- they already pass URL around in-memory.
  • The jersey checkout flow (jersey.py line 328) does NOT save session.url to Order either, but this ticket is scoped only to tournament checkout. Jersey checkout has the same gap but is out of scope -- acceptable.
  • The blast endpoint's placeholder resolution (body.data override) will continue to work for non-tournament queries that use body.data. No regression risk.

Decomposition Assessment

5 file targets in 1 repo, 5 acceptance criteria. Borderline on the 5-minute rule but all changes are tightly coupled (one new column flows through 3 touchpoints). No decomposition needed -- a single agent pass can handle this.

Recommendation

  • [SCOPE] Create architecture note arch-email for the email subsystem (email queries, blast, templates, Gmail OAuth).
  • [SCOPE] Create architecture note arch-checkout for the checkout/payment subsystem (Stripe sessions, Orders, webhooks).
Neither missing arch note blocks this ticket's readiness -- the work itself is well-scoped. These are documentation debts tracked separately (see board item #991 "Audit board items for traceability labels" and #990 "Migrate inline user stories to first-class story notes").