Review: Regenerate Utah Invitational orders + validated e2e webhook roundtrip

review-1013-2026-04-15 Review

review ready

Verdict: READY

Board item #1013 on board-westside-basketball. Forgejo issue forgejo_admin/basketball-api#484. Labels: type:feature, arch:checkout, arch:email, story:WS-S33.

Template Completeness

  • [x] Type (Feature)
  • [x] Lineage
  • [x] Repo
  • [x] User Story
  • [x] Context (webhook + helper line refs)
  • [x] Current State
  • [x] File Targets (3 NEW files)
  • [x] Files NOT to Touch
  • [x] Acceptance Criteria (5)
  • [x] Test Expectations (run command included)
  • [x] Constraints
  • [x] Checklist
  • [x] Related

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"
  • [x] story note verified — WS-S33 present in project-westside-basketball user-stories (stories-admin-list block)
  • [x] arch:checkout + arch:email labels
  • [ ] arch note MISSING — [SCOPE] No arch-checkout or arch-email notes exist in pal-e-docs. Non-blocking here (consistent with current project convention; no peer tickets carry backing arch notes either), but worth a follow-up scoping item to stand these up.
  • [x] Forgejo issue — forgejo_admin/basketball-api#484 open

File Targets

  • [x] src/basketball_api/routes/webhooks.py (reference-only) — verified. Line 197 reads order_id_str = metadata.get("order_id"). Matches ticket claim that handler reconciles via order_id metadata.
  • [x] src/basketball_api/routes/checkout.py (reference-only) — verified. create_player_checkout_session() defined at line 467 (ticket said 529-533; those lines are where the metadata dict is built and include "order_id": str(order.id)). Ticket's functional claim is correct; the cited line range refers to the metadata block inside the helper, not the def line. Acceptable.
  • [x] scripts/regenerate_tournament_orders.py — NEW; does not exist. Correct.
  • [x] tests/test_tournament_webhook_roundtrip.py — NEW; does not exist. Correct.
  • [x] docs/tournament-billing-runbook.md — NEW; docs/ exists (migrations.md, auth-architecture.md), file does not. Correct.

Repo Placement

OK. All file targets live under forgejo_admin/basketball-api. Single-repo scope; no cross-repo coordination needed.

Dependencies

  • Related: #456 (blast system), #457 (tournament checkout), #463 (blast query integration). None are listed as blockers, and the ticket operates on data those features produced. The 21 stale Orders are an artefact of #457's rollout; this ticket cleans up without depending on further work.
  • No board items in in_progress appear to block this (board_slug board-westside-basketball is large; relied on ticket's own Related links rather than exhaustively diffing the board).
  • sop-email-send (updated 2026-04-13) referenced as context for blast handoff, not as a code dependency.

Acceptance Criteria

All 5 criteria are verifiable by an agent:
  • Script deletes + regenerates against tournament_id=2 — verifiable via DB query pre/post.
  • Every new session has order_id metadata — verifiable via Stripe API list.
  • Webhook roundtrip flips pending → paid — covered by the integration test run command pytest tests/ -k "tournament_webhook or regenerate".
  • Runbook documents sequence + forbids ad-hoc sessions — verifiable by reading docs/tournament-billing-runbook.md.
Minor ambiguity: ticket says "synthesized signed" webhook event and "test-mode Stripe webhook secret", while every existing webhook test in this repo mocks stripe.Webhook.construct_event rather than exercising real HMAC verification. Dev should either (a) follow existing mock pattern and drop the "signed" language, or (b) use stripe.WebhookSignature.generate_header against a fixture secret. [BODY] Minor clarification welcome but not blocking — dev can judge from existing tests/test_webhooks.py.

Blast Radius

Flagging for visibility (not blocking this ticket):
  • The runbook rule "always use admin endpoint, never ad-hoc sessions" conflicts with 6 existing call sites of stripe.checkout.Session.create in src/: admin.py:2005, jersey.py:291, register.py:1378+1423, checkout.py:246+394+537. Each of these paths is "the committed endpoint" for its domain (jersey/register/subscription), not ad-hoc. The runbook should scope its rule to tournament-fee checkout specifically, or explicitly enumerate the sanctioned helpers per product type. Worth a sentence in the runbook.
  • Zero production data risk: ticket confirms 21 stale sessions are all expired/unpaid with zero payments collected. Safe cleanup.
  • Idempotency constraint is explicit — good.

Decomposition Assessment

3 new files, 5 AC, single repo, no migrations, no infra changes. Estimated agent work < 5 min. No decomposition needed.

Recommendation

  • [SCOPE] Follow-up ticket: create arch-checkout and arch-email notes in pal-e-docs so future tickets with these labels have backing architecture notes to reference. Not a blocker for #484.
  • [BODY] Optional clarification in #484: spell out whether "signed synthesized event" means real HMAC (via stripe.WebhookSignature.generate_header) or the existing mocked construct_event pattern used across tests/test_webhooks.py. Dev can reasonably choose either; noting it preempts review churn.
  • [BODY] Optional refinement: in the runbook's "never ad-hoc" rule, scope it to "never create Stripe sessions for tournament fees outside create_player_checkout_session()." The broader statement conflicts with sanctioned per-product helpers elsewhere in the codebase.
READY to advance to next_up. Recommendations above are nits for dev to absorb; none block execution.