Recover stranded Utah Invitational orders via regen + apology email #486
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#486
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
Task
⚠️ HARD STOP — NO EMAILS WITHOUT APPROVAL ⚠️
No email reaches any parent until ALL of the following are true:
draneylucas@gmail.comhas been received AND clicked by Lucas.Approved draft ≠ approved to send. One approval = one send. If the copy, recipient list, or timing changes between approval and send, the approval chain restarts from Lucas. This rule is non-negotiable — see
feedback_email_blast_nuclear_gate.md,feedback_no_email_without_five_approvals.md,feedback_one_approval_one_send.md, and the 2026-04-04 + 2026-04-13 incidents.Test recipients:
draneylucas@gmail.comor@example.comonly. Never a real parent address under any circumstances.Lineage
Standalone — discovered 2026-04-17 when parent Daniel Niyitanga reported a broken checkout link. Investigation confirmed 18 tournament orders stranded behind expired Stripe sessions, of which 17 are real parents and 1 is a Westside Admin test order (excluded). See sibling Bug #488 for the root-cause fix.
Refinement applied after
/review-ticketpass 1 (review notereview-1022-2026-04-17):story:WS-S22,arch:dataflow-westside-basketball).Repo
forgejo_admin/basketball-apiUser Story
As a parent who received a Utah Invitational checkout email but couldn't pay because the link expired, I receive an apology and a fresh working link so I can still pay my child's tournament fee.
Context
On 2026-04-12, an email blast sent 21 parents Stripe checkout links for Utah Invitational tournament fees across three divisions (17U Elite $55, 17U Select $65, 16U Elite $55). One additional order (94) was created Apr 15 for a late registrant (Chaoying Fan).
Stripe Checkout Sessions default to
expires_at = created + 86400s(24h). Our code never overrides this. Every session from the blast expired Apr 13. Only 5 parents clicked within the TTL and paid; 17 real-parent orders remainpendingwith dead sessions. Verified via Stripe API (status=expired,url_present=false) on a sample — 100% failure rate. Stranded revenue: $985.Daniel is the first to complain. The other 16 have silently assumed the email was a dead end.
Scope
Regenerate fresh Stripe Checkout Sessions for all 17 real-parent pending Utah Invitational orders (products 5, 6, 7) using
scripts/regenerate_tournament_orders.py --product-ids 5,6,7 --commit(after filtering out order 93). Then send an apology + new-link email to the 17 parents, gated through the blast approval SOP with no exceptions.Stranded real parents (17) — none have paid, none are on the existing paid list:
Explicitly excluded:
westsidebasktball@gmail.com, "Test Kings Player"). Do NOT regen, do NOT email.Acceptance Criteria
Phase A — Approval Gates (BLOCKING, NO CODE OR EMAIL BEFORE ALL OF THESE):
_brand_wrappersystem atservices/email.py:202) covering: what happened, apology, fresh link, action neededdraneylucas@gmail.comPhase B — Mechanical recovery (can run before Phase A gates fire, but blast in Phase C waits):
scripts/regenerate_tournament_orders.py --product-ids 5,6,7lists exactly 18 pending orders; confirm order 93 is filtered or skipped manually so regen touches only 17--commit; 17 Orders now have freshstripe_checkout_session_idmetadata.order_idpresent,status=open,expires_at≈ 30 days out (verifies #488 deploy took effect)Phase C — Execution (ONLY after every Phase A gate is green AND Phase B is complete):
westsidebasktball@gmail.com)paidvia normal webhook flowPhase D — Post-send documentation:
docs/tournament-billing-runbook.md(what was actually executed — parent list, timing, approval chain, results). This is separate from #488's root-cause correction.Constraints
feedback_email_blast_nuclear_gate.md— the 7-step gate is the floor, not a guidelinefeedback_no_email_without_five_approvals.mdfeedback_one_approval_one_send.md— if the draft changes between Gate 2 and Gate 4, restart at Gate 1_brand_wrapperMJML system (System 1, Queens branded) perfeedback_two_email_systems.md— NOT the generic MJML templates@example.comordraneylucas@gmail.comperfeedback_never_email_without_approval.mdChecklist
docs/tournament-billing-runbook.mdupdated with Phase D recovery procedure narrative (separate from #488's root-cause correction)Related
project-pal-e-platformforgejo_admin/basketball-api #488(30-dayexpires_atpatch must ship first so fresh sessions minted by #486's regen carry the durable TTL)Scope Review: NEEDS_REFINEMENT
Review note:
review-1022-2026-04-17Scope is largely solid — the 6-gate approval chain is prominent and repeated in three places (HARD STOP, Phase A AC, Constraints), and the 18-stranded / 5-paid lists reconcile 100% against live basketball-api DB state ($1,040 math verified). Script
scripts/regenerate_tournament_orders.pyand the_brand_wrapperMJML helper both exist and match the ticket's references exactly. Four fixable issues prevent advance to todo:[SCOPE]Order 93 treatment unclear. Parent is "Westside Admin", emailwestsidebasktball@gmail.com, player "Test Kings Player" — this is an internal admin test order, not a real parent. Ticket should either (a) exclude it, drop recipient count to 17 throughout (including Gate 5 read-back), or (b) keep it as intentional QA recipient and say so in Scope.[SCOPE]story:payment-recoverylabel has no matching entry inproject-westside-basketball→ stories-parent. Either addWS-S29("As a parent whose checkout link expired before I could pay…") or relabel tostory:WS-S22(existing clear-communications story).[SCOPE]arch:stripe-checkoutlabel has no backing note in pal-e-docs (searched). Existing arch notes arearch-domain/dataflow/deployment/auth-westside-basketball. Createarch-stripe-checkoutcovering the 6 Session creation paths + expires_at/metadata conventions, or relabel toarch:dataflow-westside-basketball.[BODY]Checklist item 6 says "Runbook updated… move into #488's scope to avoid PR churn" — but #488 is the Stripe TTL infra bug, not a docs ticket. Either open a separate runbook tracking item or move this clause into #488's Checklist directly (with confirmation).Not blocking: the 6-gate approval chain placement is exemplary. Three citations (
feedback_email_blast_nuclear_gate.md,feedback_no_email_without_five_approvals.md,feedback_one_approval_one_send.md) are all correct. Approval chain itself needs no refinement.Decomposition: not needed. 13 AC split across Phases A/B/C, but Phase B is <5 min of agent work and A/C are human-gated — one agent can own the whole ticket with wait states.