fix: regenerate Utah Invitational tournament orders via blessed helper #485
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!485
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "484-regenerate-utah-tournament-orders"
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
The Utah Invitational blast created 21 pending Orders whose Stripe Checkout Sessions were built by ad-hoc
stripe.checkout.Session.createcalls that omittedorder_idmetadata. When parents paid, the webhook handler atroutes/webhooks.py::_handle_generic_order_completedcould not match the session back to its Order row, so the Orders stayedpendingeven after Stripe took the money.This PR adds the blessed helper, an idempotent regen script, an end-to-end signed-webhook roundtrip test, and the operator runbook.
Changes
src/basketball_api/services/tournament_checkout.py— new helpercreate_tournament_order_checkout_session(). Single source of truth for tournament-fee sessions; always setsorder_id/player_id/product_idmetadata.scripts/regenerate_tournament_orders.py— idempotent regen script. Dry-run default,--tournament-id,--product-ids,--commit,--force. Expires old broken sessions, mints replacements through the helper.tests/test_tournament_webhook_roundtrip.py— full-path integration test. Does NOT mockstripe.Webhook.construct_event. Synthesizes a realcheckout.session.completedpayload, signs it with the test webhook secret via raw HMAC-SHA256 in the Stripet=..,v1=..scheme, POSTs to TestClient, asserts Order flipspending -> paid. Companion bad-signature case proves the real validator is in the loop.docs/tournament-billing-runbook.md— operator runbook. Prohibition on ad-hoc session creation is scoped to tournament fees only. The five other sanctionedstripe.checkout.Session.createcall sites (jersey, generic /create-session, first-payment, tryout registration x2) are documented and unaffected.QA Review Items Addressed
construct_eventis never patched. Real HMAC-SHA256 signature in Stripe'st=..,v1=..scheme is validated by the real SDK inside the route. Bad-signature test confirms validator is active.Test Plan
pytest tests/test_tournament_webhook_roundtrip.py -v— 2 passed locallypytest tests/test_webhooks.py tests/test_checkout.py tests/test_tournament_webhook_roundtrip.py— 37 passed locally (no regressions in adjacent tests)ruff format+ruff check— clean on new filespython scripts/regenerate_tournament_orders.py --helpprints expected usageReview Checklist
ruff format+ruff checkclean on new filesconstruct_eventmock)Related Notes
src/basketball_api/routes/webhooks.py::_handle_generic_order_completedCo-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com
PR #485 Review
DOMAIN REVIEW (Python / FastAPI / SQLAlchemy / Stripe)
src/basketball_api/services/tournament_checkout.py— solidorder_id/player_id/product_idmetadata.CheckoutSessionResultdataclass is a nice typed boundary._ensure_customercreates-or-reuses correctly and persistsstripe_customer_id.ValueErrorwhen relationships missing — good defensive check._SUCCESS_URL/_CANCEL_URLcaptured at import time from_settings.frontend_url. If settings ever mutate in tests this could bite, but current codebase treats settings as immutable at boot — acceptable.scripts/regenerate_tournament_orders.py— meets spec--commitrequired to write. Correct safety posture.--tournament-id,--product-ids,--forceall present as requested._session_has_order_id()guard;--forcebypass._expire_session()is best-effort with typed exception handling (InvalidRequestError) and broad fallback. Good.create_tournament_order_checkout_session) — no ad-hoc Session.create. Correct.Order.custom_data["tournament_id"].as_integer()— non-portable (PG-specific JSON accessor). Fine for a prod-run script, but if you ever want this script under pytest with SQLite it will error. Not a blocker.--commit. Correct.tests/test_tournament_webhook_roundtrip.py— excellentt=..,v1=..scheme. Nostripe.Webhook.construct_eventpatch. Exactly what the incident review demanded.stripe_webhook_secret == TEST_WEBHOOK_SECRETbefore running — fails loud if conftest env drifts.Customer.create,checkout.Session.create). Correct surface.Order.statusflip pending->paid ANDstripe_payment_intent_idcaptured. Complete assertion.docs/tournament-billing-runbook.md— scoped correctlystripe.checkout.Session.createcall sites enumerated with file:line references (jersey, generic /create-session, first-payment, tryout x2). Operators cannot confuse scope.BLOCKERS
Scope creep —
skip_prorationchanges are unrelated to issue #484. The following changes belong in a separate PR tied to the Jaxon skip_proration work (session memory references #458):src/basketball_api/routes/admin.py—skip_prorationquery param on/admin/email/first-paymentsrc/basketball_api/routes/checkout.py—proratequery param on/checkout/first-paymentsrc/basketball_api/services/email.py—send_first_payment_email(skip_proration=...)tests/test_first_payment.py,test_first_payment_blast.py,test_first_payment_email.py— tests for the aboveIssue #484 is "regenerate Utah tournament orders." Bundling the skip_proration feature here violates per-convention "one ticket = one PR" (feedback_smaller_scopes_parallel). Pull skip_proration into its own branch/PR referencing its own ticket.
Python 3.12+ f-string syntax risk in
services/email.py. Two places now use nested f-strings with the same quote character:and in
plain_body:This requires PEP 701 (Python 3.12+). If CI or prod runs 3.11, this is a syntax error. Verify the runtime Python version is >=3.12 OR refactor to build the conditional fragment in a plain variable before the f-string. The
chr(10)usage is also a code smell — prefer a"\n\n"literal held in a variable.NITS
services/email.py: the conditional HTML and plain-text branches duplicate logic. Consider extracting a small_build_fee_copy(skip_proration, monthly_fee)helper returning a tuple so the templates stay readable. Tied to the blocker above — fix when you split the scope.regenerate_tournament_orders.py:stripe.error.InvalidRequestErrorimport pattern uses# type: ignore[attr-defined]. Fine, but a top-of-filefrom stripe.error import InvalidRequestErrorwould be cleaner and let you drop the ignore.TEST_WEBHOOK_SECRET = "whsec_test"— consider reading it from settings directly to remove the duplication (the assert guards drift, but DRY is cheap here).Closes #484in "Related Notes" — good. Confirm Forgejo auto-closes on merge.SOP COMPLIANCE
484-regenerate-utah-tournament-orders)create_player_checkout_session()-equivalent pattern (actually introducescreate_tournament_order_checkout_session()as the blessed helper — naming is clearer than the issue's suggested name, consistent with scope)PROCESS OBSERVATIONS
git revertalso reverts proration changes (and vice versa). DORA CFR suffers. Split now, merge separately.sop-webhook-roundtrip-testingnote in pal-e-docs.VERDICT: NOT APPROVED
Split the
skip_prorationchanges into a separate PR tied to its own issue (#458 or a new ticket). After split, verify the remaining Python 3.12+ f-string risk is mitigated (either confirm runtime is 3.12+, or — cleanest — the email changes leave this PR entirely). The tournament regeneration work itself is well-executed and ready to merge once scoped cleanly.e720a88f6cf16fd1e9ef