Review: Regenerate Utah Invitational orders + validated e2e webhook roundtrip
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 viaorder_idmetadata. - [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_idmetadata — 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.createin 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 mockedconstruct_eventpattern 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.