Mint Stripe Payment Links for tournament-fee email blasts (replaces expiring Checkout Sessions) #494
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#494
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Type
Feature
Lineage
Standalone — surfaced 2026-04-17 after PR #490 (issue #488) proved that
expires_aton Checkout Sessions cannot exceed Stripe's 24h cap. Architecture pivot from "extend the TTL" to "use a primitive that doesn't have a TTL." See#489spike, which this implementation dogfoods.Repo
forgejo_admin/basketball-apiUser Story
As a parent who receives a tournament-fee email, I can click the link any time after receipt — that day, that week, or a month later — and reach a working Stripe Checkout page. The link does not expire out from under me.
Context
We currently send Stripe Checkout Session URLs in tournament-fee email blasts. Those URLs are hard-capped by Stripe at a 24-hour lifetime. Real parent email-open latency is often days, so most blast recipients hit dead links. The 2026-04-17 Utah Invitational incident stranded 17 parents behind expired sessions (~$985); attempts to extend
expires_atto 30 days broke prod outright because Stripe rejects >24h on Checkout Sessions.Stripe Payment Links (
stripe.PaymentLink) are a different Stripe product designed exactly for this use case: URLs that persist in inboxes for days or weeks without expiring, carry metadata, and fire the samecheckout.session.completedwebhook we already handle. Fulfillment code does not need to change — only the primitive that mints the URL.Scope is limited to the tournament-fee flow first. Monthly and jersey blasts follow in separate tickets once this pattern is validated by the Utah Invitational recovery dogfood (see
#486).File Targets
Dev agent's decision — grep live code for the current tournament-fee mint path (helper + any admin regen handler) and substitute the primitive. Do not touch non-tournament flows.
Acceptance Criteria
order_idpresent in Stripe metadata so the existing webhook matcher continues to mark the Order paid#486can mint 17 Utah Invitational links via this helper instead of the legacy regen script, and parents receive working linksTest Expectations
order_id→Order.status = paidon payment (assert against mocked webhook event)expires_at. Prevents the class of bug we just hit with mocked-only coverage.Constraints
Checklist
#486recovery ticket with no additional plumbingRelated
forgejo_admin/basketball-api #486— Utah Invitational recovery. First consumer of this helper. Dogfood.forgejo_admin/basketball-api #489— architecture spike. This ticket is the implementation path for the spike's recommended answer.forgejo_admin/basketball-api #493— revert of broken #490 code. Lands before or alongside this ticket.forgejo_admin/basketball-api #488— original (misdiagnosed) ticket. Superseded.feedback_retrieve_before_theorize.md— the Stripe hard-fact table at the bottom.forgejo_admin/claude-custom #242— SOP/template fix that would let future tickets skip these prescribed sections.