Stale pending orders permanently block checkout — need auto-expiry and cleanup #371
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#371
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
Bug
Lineage
Discovered from parent reports (Gracie Maloney-Holland, 2026-04-07). 6 stale orders manually canceled during investigation. Root cause: Stripe webhook was broken for weeks (forgejo_admin/basketball-api#350), leaving Order records permanently
pending.Repo
forgejo_admin/basketball-apiWhat Broke
The generic checkout endpoint
POST /checkout/create-session(line 149-166 ofsrc/basketball_api/routes/checkout.py) blocks if a player has ANYpendingorpaidOrder for the same product. When a Stripe checkout session is created but payment never completes (user abandons, webhook fails, session expires), the Order stayspendingforever. The user cannot try again — they see "Player already has an active order for this product (order #N)".Affected users so far: Gracie Maloney-Holland (Orders #20, #21), Jacelyn Laila Bronson (#1), David Kaneko (#3), Mateus Rigitano de Paula (#5), Anaiyah Fesolai (#16). All manually canceled on 2026-04-07.
Architecture context — two checkout systems exist:
/jersey/checkout— writes toPlayer.jersey_order_statusdirectly, no Order table, no duplicate blocking. Called by/jerseypage./checkout/create-session— writes to Order table, has duplicate blocking that treatspendingas active. Called by/checkoutpage. This is where the bug is.Both pages are live. Parents may land on either depending on which link they follow.
Repro Steps
pendingwith nostripe_payment_intent_idExpected Behavior
Abandoned checkout sessions should not permanently block the user. Three fixes needed:
Fix 1: Auto-expire stale orders. Orders that are
pendingwith nostripe_payment_intent_idfor >1 hour should auto-cancel. Implement as either:create-sessionbefore the duplicate query (cancel stale orders first)Fix 2: Don't treat
pendingas blocking. The duplicate check on line 149-166 should only block onpaidorders, notpending. A user should be able to retry checkout even if a previous attempt is pending. The webhook handles idempotency — if the old session somehow completes, it creates/updates the order via webhook.Fix 3: Add cancel-order endpoint.
POST /checkout/cancel-order?order_id=N&token=...— allows users to explicitly cancel a pending order from the frontend. The cancel page (/checkout/cancel) currently does nothing with the Order record.Environment
POST /checkout/create-session(blocking bug),GET /checkout/cancel(no-op)pendingorders found and manually canceledAcceptance Criteria
canceledRelated
project-westside-basketball— project this affectsstory:WS-S18— parent jersey orderingforgejo_admin/basketball-api#350— webhook fix that caused the original stale orderssrc/basketball_api/routes/checkout.py:149-166(duplicate check),src/basketball_api/models.py(Order/OrderStatus)