Review: #488 Set expires_at=30d on all 6 Stripe Session create calls
Verdict: NEEDS_REFINEMENT
Board item: #1023 (board-westside-basketball, column=backlog)
Forgejo issue: forgejo_admin/basketball-api#488 — open
Base commit audited: origin/main @ a4047d2 (fd081e0 ancestor confirmed)
Issue type: Bug (template-issue-bug)
Forgejo issue: forgejo_admin/basketball-api#488 — open
Base commit audited: origin/main @ a4047d2 (fd081e0 ancestor confirmed)
Issue type: Bug (template-issue-bug)
Template Completeness
- [x] Type — "Bug"
- [x] Lineage — references #486, discovery narrative present
- [x] Repo — forgejo_admin/basketball-api
- [x] What Broke — Stripe API evidence + stranded revenue quantified
- [x] Repro Steps — both inline code path and Stripe API retrieve path
- [x] Expected Behavior — 30-day TTL, shared constant suggestion, runbook correction requirement
- [x] Environment — commit pin, namespace, affected data counts
- [x] Acceptance Criteria — 7 items, each verifiable
- [x] Related — #486 (recovery), #487 (metric), pal-e-platform#295 (alert)
All sections present.
Traceability
- [x] story:payment-reliability label — present on board item
- [ ] story note MISSING — project-westside-basketball user-stories uses
WS-S{N}convention (WS-S1 through WS-S33). The labelpayment-reliabilityis a bare slug that doesn't match the project's story taxonomy. The closest existing story isWS-S11("As an admin, I want to create Stripe checkout sessions for jerseys/fees so that payments are automated"). Note: siblings #486 (story:payment-recovery) and #487 (story:observability) share the same pattern — the whole payment-pipeline story cluster is off-taxonomy. - [x] arch:stripe-checkout label — present on board item
- [ ] arch note MISSING —
search_notes(query="arch-stripe-checkout")returned zero results. No backing architecture note exists in pal-e-docs. - [x] Forgejo issue — https://forgejo.tail5b443a.ts.net/forgejo_admin/basketball-api/issues/488 is open and contains the full spec.
File Targets
Audited against
origin/main @ a4047d2. Grep for stripe\.checkout\.Session\.create across src/ returned 8 call sites, not 6.- [x]
src/basketball_api/services/tournament_checkout.py:78— verified,stripe.checkout.Session.create(at line 78 - [x]
src/basketball_api/routes/jersey.py:291— verified, call is at line 291 - [ ]
src/basketball_api/routes/checkout.py:246— ISSUE: line 246 is the comment# Create Stripe Checkout Session; the actual call is at line 247. Off by one. - [ ]
src/basketball_api/routes/checkout.py:398— ISSUE: line 398 isorder = Order(...); the actual call is at line 410. Off by 12 lines. - [ ]
src/basketball_api/routes/register.py:1373— ISSUE: line 1373 isamount_cents = 3000 if reg_type == "tryout" else 4000; the actual call is at line 1378. Off by 5 lines. - [ ]
src/basketball_api/routes/register.py:1418— ISSUE: line 1418 isamount_cents = 3000 if reg_type == "tryout" else 4000; the actual call is at line 1423. Off by 5 lines. - [ ] MISSING CALL SITE:
src/basketball_api/routes/checkout.py:553— confirmedstripe.checkout.Session.create(call. Appears to be a helper path that builds the sharedorder_id/player_id/product_idmetadata and then mints a session. Not referenced in the ticket. - [ ] MISSING CALL SITE:
src/basketball_api/routes/admin.py:2005— confirmedstripe.checkout.Session.create(call inside an admin "regenerate session" handler (preceded by# Create a fresh Stripe Checkout Session). Not referenced in the ticket.
This is a
feedback_verify_repo_layout_before_ticketing.md violation. The ticket was authored against memory or a stale snapshot. The "all 6" count is wrong — there are 8 sites — and 4 of the listed line numbers are stale. This matters because:- The ticket title says "all 6," the first acceptance criterion says "All 6 call sites," and the regression test AC says "parametrizes across all 6 routes/helpers." If a Dev agent trusts the count,
admin.py:2005andcheckout.py:553will be left unfixed and the test will pass while still shipping two 24-hour-TTL sessions. - Bad line numbers slow Dev agents down but do not break correctness (they will locate the call sites by pattern). Bad counts do break correctness.
Repo Placement
OK. All 8 call sites live in
forgejo_admin/basketball-api. No cross-repo scope.Dependencies
- Sibling #486 (board item #1022) — recovery blast for the 18 stranded Utah Invitational orders. #486's body explicitly says the regen script re-mints sessions via the blessed helper; if #488 lands first, those regenerated sessions inherit the new 30-day TTL. If #486 ships first, parents get regenerated links that still expire in 24h and the whole recovery blast wastes one approval cycle. #488 is correctly ordered before #486. Dev agent must not start #486's regen step until #488 is merged and deployed.
- Sibling #487 (board item #1024) — expired-session metric, observability follow-up. Independent; can run in parallel.
- Sibling #489 (board item #1025) — spike comparing Payment Links vs lazy-mint vs 30d TTL. #488 is the interim fix; #489 picks the long-term pattern. Don't couple them.
- Commit fd081e0 (#480) — "require amount match when reusing fresh pending Stripe session" touched Stripe session reuse logic. Confirmed this commit is ancestor of current HEAD; no rebase needed.
Acceptance Criteria
Each AC is verifiable by the Dev/QA agent. However:
- AC-1 "All 6 call sites" — must be updated to "All 8 call sites" (see File Targets).
- AC-2 "parametrizes across all 6 routes/helpers" — must be updated to 8.
- AC-5 Runbook update — adequately scoped. The runbook's sanctioned-call-sites table (lines 23-29) is also stale: it lists 5 non-tournament sites with the same outdated line numbers the ticket has, and omits
routes/checkout.py:553androutes/admin.py:2005. Dev agent should fix the table row list + line numbers in the same PR as the root-cause narrative fix. - AC-7 "expires_at - created ≈ 2592000s" — math confirmed: 30 × 24 × 3600 = 2,592,000, which is Stripe's maximum for payment mode. Correct value.
- Consider adding: "Shared constant
CHECKOUT_SESSION_TTL_SECONDSlives in exactly one module (suggestsrc/basketball_api/stripe_config.pyor similar) and is imported at all 8 sites — no duplicated literal." This hardens the "one knob" invariant and makes the spike #489's cutover easier.
Blast Radius
- Regression test file:
tests/test_checkout_session_ttl.pydoes not exist in main — safe to create. Existingtests/test_checkout.pyis unrelated (generic checkout coverage) and won't collide. - Non-tournament flow risk: 30-day TTL is benign for all 8 sites. Stripe sessions are only "active" until paid or expired; a longer TTL just keeps more expired links reachable, which is the desired behavior for blast-delivered URLs.
- Webhook handlers: No change.
expires_atis a client-side mint parameter; webhook signatures andcheckout.session.completedsemantics are untouched. - Runbook blast radius: The
order_id-metadata narrative is wrong on the Utah incident (ticket correctly flags this), butorder_idmetadata is still a real correctness invariant for tournament flows — the "forbidden ad-hoc create" rule in the runbook's TL;DR should be preserved, only the "What broke" section needs the TTL rewrite + move the metadata hypothesis to a "Historical note" subsection. Ticket AC already says this explicitly. - Other repos:
Grepconfirms nostripe.checkout.Session.createcalls in westside-app, westside-contracts, or pal-e-app. basketball-api is the only mint point.
Decomposition Assessment
8 call sites in a single repo + 1 runbook + 1 new test file. All changes share one invariant ("pass
expires_at"). Mechanical edit with obvious pattern. Estimated agent work: ~5 minutes for the code + test + runbook rewrite. No decomposition needed — single Dev agent pass is appropriate.Recommendation
[BODY]Fix the count: replace "all 6" with "all 8" in the title, the "Affected call sites (all 6)" header, AC-1 ("All 6 call sites"), and AC-2 ("parametrizes across all 6 routes/helpers").[BODY]Fix the file-target list to the verified lines onorigin/main @ a4047d2:[BODY]Add explicit note in AC-5 thatdocs/tournament-billing-runbook.md's "sanctioned call sites" table (currently lines 23-29) must also be updated to reflect the 7 non-tournament sites with correct line numbers — not just the "What broke" narrative.[BODY](Optional hardening) Add AC: "A singleCHECKOUT_SESSION_TTL_SECONDS = 30 * 24 * 3600constant lives in one module and is imported at all 8 call sites; no inline duplication of the literal." Makes the spike #489's future cutover a one-line change.[SCOPE]Create architecture notearch-stripe-checkoutcovering the 8-site mint surface, TTL invariant, webhook handler routing (order_idmetadata contract), and the relationship to #489's pattern evaluation. This is also missing for siblings #486 (arch:stripe-checkout) and #487 (arch:payment-pipeline) — one note likely covers all three.[SCOPE]Reconcile story taxonomy: either addWS-S34(or similar) toproject-westside-basketballuser-stories section with a payment-reliability narrative, OR retag #488/#486/#489 with an existing WS-S* story (WS-S11 is the nearest fit). Same reconciliation applies to #487'sstory:observability. This is a project-wide convention drift, not a #488-specific problem, but #488 inherits it.
Blocking refinements: the two
[BODY] corrections on count + line numbers. The [SCOPE] items can land in parallel (or as follow-up board items) and should not hold the Bug fix — the Utah incident is fresh and #486 depends on this patch.