Recover stranded monthly-fee parents (investigate + apology blast, analog of #486) #497

Open
opened 2026-04-18 04:45:13 +00:00 by forgejo_admin · 4 comments

Type

Task

⚠️ HARD STOP — NO EMAILS WITHOUT APPROVAL ⚠️

No email reaches any parent until ALL of the following are true:

  1. Lucas has explicitly approved the final email draft.
  2. Marcus has explicitly approved the final email draft.
  3. A test email sent to draneylucas@gmail.com has been received AND clicked by Lucas.
  4. Lucas has said "BLAST" once to trigger.
  5. Ava has read back subject + recipient count + an example recipient name.
  6. Lucas has said "BLAST" a second time to confirm.

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.com or @example.com only. Never a real parent address under any circumstances.

Lineage

Standalone — emerged from 2026-04-17 Utah Invitational recovery (#486). The same 24h Stripe Checkout Session expiry that stranded 17 tournament parents also affects monthly-fee payment emails. Architecture migration to Payment Links is a sibling Feature ticket (#498).

Refinement applied after /review-ticket pass 1 (review note review-1033-2026-04-17):

  • Nuclear gate reproduced verbatim (was collapsed to a single checkbox — unacceptable per feedback_email_blast_nuclear_gate).
  • Phase A AC expanded into per-parent classification structure.
  • Hard constraint added: no email drafting during Phase A.
  • Phase C (observability) kept in ticket but with tighter AC and explicit scope cap.
  • Full Checklist section added.
  • Decomposition considered and rejected: the 2026-04-17 audit (agent a1a654212264ebceb) classified the blast radius at 4 real parents / $455 (baseline of 5 orders / $460 dilated when order 79 was identified as the Westside Admin test account). At this size, one-ticket recovery is correct.

Repo

forgejo_admin/basketball-api

User Story

As a parent who received a monthly-fee payment email and whose link expired before I could click it, I receive an apology and a fresh working link so I can pay my player's monthly dues without having to chase Marcus.

Context

Monthly-fee flow still uses stripe.checkout.Session.create with Stripe's default 24-hour expires_at — the same mechanism that stranded 17 tournament parents. The only monthly-fee mint site as of 2026-04-17 is src/basketball_api/routes/checkout.py:410 inside first_payment_checkout (def at line 306), verified by the audit agent. Architecture migration to Payment Links is tracked in #498 (Feature).

Blast-radius audit results (2026-04-17, read-only Stripe retrieve across all product 4 orders):

Category Count Dollars Notes
TRUE_STRANDED 4 parents $455 Recovery pool
NORMAL_PENDING 1 parent $150 Expires within 1h of audit — re-audit next day before including
PAID_VIA_RETRY 3 parents Self-recovered after 2–4 expired sessions; no action
PAID_ORIGINAL 3 parents Paid on first try; no action
TEST_ACCOUNT 1 (parent 170) Westside Admin; excluded

True-stranded parents (4) — all subscription_status=none, contract_status=signed, still rostered:

Order Parent Player Email Amount Age
82 Creamson Kaneko David Kaneko kanekodavid4@gmail.com $165 4.1d
50 fatkid816 Jaxon Gerber fatkid816@gmail.com $40 5.2d
28 Sarah Silva Sarah Lédio da Silva lediosilvasarah@gmail.com $85 5.4d
27, 75, 76 Vince Ifote Ifote Vince Ifote vinceifote25@gmail.com $165 4.2d

All four have Stripe sessions returning status=expired, payment_status=unpaid, url_present=false. Order 79 from the initial spot-check is excluded (Westside Admin test, parent_id=170).

Pending re-audit: Order 95 (Artyom Litvinau / Arseni Litvinau, $150). At audit time the session was open with ~0.7h remaining. Re-audit this order the day of Phase A execution — if expired, add to recovery pool; if paid, exclude.

Scope

Phase A — Investigation + Recipient Finalization (first deliverable, no drafting).
Phase B — Recovery blast (nuclear-gated, starts ONLY after Lucas signs off on the Phase A dry-run).
Phase C — Observability (recurring paid-vs-abandoned report so this incident class surfaces via data before a parent complaint).

Acceptance Criteria

Phase A — Investigation + Recipient Finalization (NO EMAIL WORK IN THIS PHASE):

  • Re-retrieve Stripe state for the 4 TRUE_STRANDED orders (27/75/76, 28, 50, 82) to confirm no self-recovery since 2026-04-17 audit
  • Re-retrieve Stripe state for order 95 (Artyom); classify as STRANDED, PAID, or NORMAL_PENDING based on current state
  • Full per-parent classification table delivered: parent_id, parent_name, player_name, email, latest_order_id, amount_cents, classification (TRUE_STRANDED / PAID_VIA_RETRY / NORMAL_PENDING / WITHDRAWN / TEST_ACCOUNT), rationale
  • Cross-reference with active roster / subscription status — any parent with subscription_status != none or withdrawn-flag / unsigned-contract is excluded from the recovery pool
  • Dry-run recipient list presented to Lucas BEFORE any email template drafting
  • Lucas explicitly approves the recipient list (names + count + dollar total)
  • Phase A deliverable logged as a pal-e-docs note referenced from this ticket

Phase B — Approval Gates (BLOCKING, NO CODE OR EMAIL BEFORE ALL OF THESE):

  • Apology email template drafted (MJML, Queens pink _brand_wrapper system at services/email.py:202) covering: what happened, apology, fresh link, action needed
  • Lucas approves email draft (Gate 1)
  • Marcus approves email draft (Gate 2)
  • Test email sent to draneylucas@gmail.com
  • Lucas confirms test email received + clicked link + verified Stripe Checkout renders (Gate 3)
  • Lucas says "BLAST" (Gate 4 — trigger)
  • Ava reads back: subject line + recipient count + example recipient name (Gate 5)
  • Lucas confirms "BLAST" a second time (Gate 6 — final confirmation)

Phase B — Mechanical recovery:

  • #498 merged + deployed (monthly Payment Link helper) — recovery uses Payment Links, not new Sessions. If #498 is not deployed in time, fall back to 30-day-TTL Sessions via the same helper #486 used (do NOT mint default 24h sessions).
  • Dry-run recipient list confirmed (matches Phase A approved list exactly)
  • Fresh Payment Links (or TTL-extended Sessions) minted — each Order has stripe_payment_link_id (or new stripe_checkout_session_id with expiry > 7 days) and metadata.order_id present
  • Spot-check 2 orders in Stripe dashboard: metadata.order_id present, link/session active

Phase B — Execution (ONLY after every Gate is green AND mechanical recovery is complete):

  • Email sent via gmail-sdk / basketball-api blast endpoint (Gmail OAuth on westsidebasktball@gmail.com)
  • Monitor webhook logs: verify at least the first payment flips its order to paid
  • Post-send: confirm to Lucas how many sent, how many bounced (if any), first click time

Phase C — Observability (small scope, can ship independently):

  • One recurring job (k8s CronJob OR basketball-api admin endpoint) that reports, per product category (monthly, tournament, jersey, tryout, generic): count of orders with status=pending AND session.status=expired (or Payment Link active=false with no paid fulfillment)
  • Output surface: either an admin dashboard page, a weekly GroupMe/email digest to Marcus+Lucas, or a Grafana panel — pick one; do not build all three
  • Threshold alert: if stranded count exceeds 0 for monthly or tournament categories, surface it (channel chosen above)

Phase D — Post-send documentation:

  • Append recovery procedure narrative to docs/monthly-billing-runbook.md (or create if absent) — parent list, timing, approval chain, results

Constraints

  • Follow feedback_email_blast_nuclear_gate.md — the 6-step gate is the floor, not a guideline
  • Follow feedback_no_email_without_five_approvals.md
  • Follow feedback_one_approval_one_send.md — if the draft changes between Gate 2 and Gate 4, restart at Gate 1
  • Use existing _brand_wrapper MJML system (System 1, Queens branded) per feedback_two_email_systems.md — NOT the generic MJML templates
  • Phase A produces NO email drafts. Drafting begins only in Phase B after Lucas approves the recipient list.
  • Phase B soft-blocked by #498 (Payment Link helper). If #498 slips, fall back to 30-day TTL sessions — do not mint default 24h Sessions under any circumstance.
  • Phase C MUST NOT expand into a generic analytics/reporting feature. Single output channel, single metric class.
  • Do NOT touch paid-original parents (Eric Porter, Spencer Thorn, Magdaline) or paid-via-retry parents (Marcel, Umu Mailei, zacbod1709) — they self-recovered; emailing them would be noise.
  • Do NOT touch parent 170 (Westside Admin test) under any condition.
  • Test recipients use @example.com or draneylucas@gmail.com per feedback_never_email_without_approval.md
  • If any Gate fails or ambiguity surfaces, STOP. Escalate to Lucas. Do not retry unilaterally.

Checklist

  • Phase A classification note created in pal-e-docs and linked here
  • Dry-run recipient list shared with Lucas (Phase A gate)
  • Email template PR opened + reviewed (code review of the MJML template is separate from content approval)
  • All 6 approval gates completed end-to-end in order
  • Mechanical recovery (Payment Links or 30-day Sessions) completed with spot-check
  • Blast sent
  • At least 1 payment confirmed post-send
  • Phase C observability surface shipped (one channel, one metric class)
  • docs/monthly-billing-runbook.md updated with Phase D recovery procedure narrative
  • forgejo_admin/basketball-api #486 — Utah Invitational recovery. Pattern for this ticket's nuclear gate + phased structure.
  • forgejo_admin/basketball-api #498 — monthly-fee Payment Link migration (soft dependency for Phase B mechanical recovery).
  • Audit artifacts (host-side, 2026-04-17): /tmp/monthly_orders_clean.json, /tmp/monthly_orders_enriched.json, /tmp/classify.py, /tmp/summarize_monthly.py, /tmp/retrieve_sessions.py.
  • Memory: feedback_retrieve_before_theorize.md, feedback_email_blast_nuclear_gate.md, feedback_no_email_without_five_approvals.md, feedback_one_approval_one_send.md.
### Type Task ### ⚠️ HARD STOP — NO EMAILS WITHOUT APPROVAL ⚠️ **No email reaches any parent until ALL of the following are true:** 1. Lucas has explicitly approved the final email draft. 2. Marcus has explicitly approved the final email draft. 3. A test email sent to `draneylucas@gmail.com` has been received AND clicked by Lucas. 4. Lucas has said "BLAST" once to trigger. 5. Ava has read back subject + recipient count + an example recipient name. 6. Lucas has said "BLAST" a second time to confirm. **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.com` or `@example.com` only. Never a real parent address under any circumstances. ### Lineage Standalone — emerged from 2026-04-17 Utah Invitational recovery (#486). The same 24h Stripe Checkout Session expiry that stranded 17 tournament parents also affects monthly-fee payment emails. Architecture migration to Payment Links is a sibling Feature ticket (#498). Refinement applied after `/review-ticket` pass 1 (review note `review-1033-2026-04-17`): - Nuclear gate reproduced verbatim (was collapsed to a single checkbox — unacceptable per `feedback_email_blast_nuclear_gate`). - Phase A AC expanded into per-parent classification structure. - Hard constraint added: no email drafting during Phase A. - Phase C (observability) kept in ticket but with tighter AC and explicit scope cap. - Full Checklist section added. - Decomposition considered and rejected: the 2026-04-17 audit (agent a1a654212264ebceb) classified the blast radius at **4 real parents / $455** (baseline of 5 orders / $460 dilated when order 79 was identified as the Westside Admin test account). At this size, one-ticket recovery is correct. ### Repo `forgejo_admin/basketball-api` ### User Story As a parent who received a monthly-fee payment email and whose link expired before I could click it, I receive an apology and a fresh working link so I can pay my player's monthly dues without having to chase Marcus. ### Context Monthly-fee flow still uses `stripe.checkout.Session.create` with Stripe's default 24-hour `expires_at` — the same mechanism that stranded 17 tournament parents. The only monthly-fee mint site as of 2026-04-17 is `src/basketball_api/routes/checkout.py:410` inside `first_payment_checkout` (def at line 306), verified by the audit agent. Architecture migration to Payment Links is tracked in #498 (Feature). **Blast-radius audit results (2026-04-17, read-only Stripe retrieve across all product 4 orders):** | Category | Count | Dollars | Notes | |---|---|---|---| | TRUE_STRANDED | 4 parents | $455 | Recovery pool | | NORMAL_PENDING | 1 parent | $150 | Expires within 1h of audit — re-audit next day before including | | PAID_VIA_RETRY | 3 parents | — | Self-recovered after 2–4 expired sessions; no action | | PAID_ORIGINAL | 3 parents | — | Paid on first try; no action | | TEST_ACCOUNT | 1 (parent 170) | — | Westside Admin; excluded | **True-stranded parents (4) — all `subscription_status=none, contract_status=signed`, still rostered:** | Order | Parent | Player | Email | Amount | Age | |---|---|---|---|---|---| | 82 | Creamson Kaneko | David Kaneko | kanekodavid4@gmail.com | $165 | 4.1d | | 50 | fatkid816 | Jaxon Gerber | fatkid816@gmail.com | $40 | 5.2d | | 28 | Sarah Silva | Sarah Lédio da Silva | lediosilvasarah@gmail.com | $85 | 5.4d | | 27, 75, 76 | Vince Ifote Ifote | Vince Ifote | vinceifote25@gmail.com | $165 | 4.2d | All four have Stripe sessions returning `status=expired, payment_status=unpaid, url_present=false`. Order 79 from the initial spot-check is excluded (Westside Admin test, parent_id=170). **Pending re-audit:** Order 95 (Artyom Litvinau / Arseni Litvinau, $150). At audit time the session was `open` with ~0.7h remaining. Re-audit this order the day of Phase A execution — if expired, add to recovery pool; if paid, exclude. ### Scope **Phase A — Investigation + Recipient Finalization** (first deliverable, no drafting). **Phase B — Recovery blast** (nuclear-gated, starts ONLY after Lucas signs off on the Phase A dry-run). **Phase C — Observability** (recurring paid-vs-abandoned report so this incident class surfaces via data before a parent complaint). ### Acceptance Criteria **Phase A — Investigation + Recipient Finalization (NO EMAIL WORK IN THIS PHASE):** - [ ] Re-retrieve Stripe state for the 4 TRUE_STRANDED orders (27/75/76, 28, 50, 82) to confirm no self-recovery since 2026-04-17 audit - [ ] Re-retrieve Stripe state for order 95 (Artyom); classify as STRANDED, PAID, or NORMAL_PENDING based on current state - [ ] Full per-parent classification table delivered: parent_id, parent_name, player_name, email, latest_order_id, amount_cents, classification (TRUE_STRANDED / PAID_VIA_RETRY / NORMAL_PENDING / WITHDRAWN / TEST_ACCOUNT), rationale - [ ] Cross-reference with active roster / subscription status — any parent with `subscription_status != none` or withdrawn-flag / unsigned-contract is excluded from the recovery pool - [ ] Dry-run recipient list presented to Lucas BEFORE any email template drafting - [ ] Lucas explicitly approves the recipient list (names + count + dollar total) - [ ] Phase A deliverable logged as a pal-e-docs note referenced from this ticket **Phase B — Approval Gates (BLOCKING, NO CODE OR EMAIL BEFORE ALL OF THESE):** - [ ] Apology email template drafted (MJML, Queens pink `_brand_wrapper` system at `services/email.py:202`) covering: what happened, apology, fresh link, action needed - [ ] **Lucas approves email draft (Gate 1)** - [ ] **Marcus approves email draft (Gate 2)** - [ ] Test email sent to `draneylucas@gmail.com` - [ ] **Lucas confirms test email received + clicked link + verified Stripe Checkout renders (Gate 3)** - [ ] **Lucas says "BLAST" (Gate 4 — trigger)** - [ ] **Ava reads back: subject line + recipient count + example recipient name (Gate 5)** - [ ] **Lucas confirms "BLAST" a second time (Gate 6 — final confirmation)** **Phase B — Mechanical recovery:** - [ ] #498 merged + deployed (monthly Payment Link helper) — recovery uses Payment Links, not new Sessions. If #498 is not deployed in time, fall back to 30-day-TTL Sessions via the same helper #486 used (do NOT mint default 24h sessions). - [ ] Dry-run recipient list confirmed (matches Phase A approved list exactly) - [ ] Fresh Payment Links (or TTL-extended Sessions) minted — each Order has `stripe_payment_link_id` (or new `stripe_checkout_session_id` with expiry > 7 days) and `metadata.order_id` present - [ ] Spot-check 2 orders in Stripe dashboard: `metadata.order_id` present, link/session active **Phase B — Execution (ONLY after every Gate is green AND mechanical recovery is complete):** - [ ] Email sent via gmail-sdk / basketball-api blast endpoint (Gmail OAuth on `westsidebasktball@gmail.com`) - [ ] Monitor webhook logs: verify at least the first payment flips its order to `paid` - [ ] Post-send: confirm to Lucas how many sent, how many bounced (if any), first click time **Phase C — Observability (small scope, can ship independently):** - [ ] One recurring job (k8s CronJob OR basketball-api admin endpoint) that reports, per product category (monthly, tournament, jersey, tryout, generic): count of orders with `status=pending` AND `session.status=expired` (or Payment Link `active=false` with no paid fulfillment) - [ ] Output surface: either an admin dashboard page, a weekly GroupMe/email digest to Marcus+Lucas, or a Grafana panel — pick one; do not build all three - [ ] Threshold alert: if stranded count exceeds 0 for monthly or tournament categories, surface it (channel chosen above) **Phase D — Post-send documentation:** - [ ] Append recovery procedure narrative to `docs/monthly-billing-runbook.md` (or create if absent) — parent list, timing, approval chain, results ### Constraints - Follow `feedback_email_blast_nuclear_gate.md` — the 6-step gate is the floor, not a guideline - Follow `feedback_no_email_without_five_approvals.md` - Follow `feedback_one_approval_one_send.md` — if the draft changes between Gate 2 and Gate 4, restart at Gate 1 - Use existing `_brand_wrapper` MJML system (System 1, Queens branded) per `feedback_two_email_systems.md` — NOT the generic MJML templates - **Phase A produces NO email drafts.** Drafting begins only in Phase B after Lucas approves the recipient list. - Phase B soft-blocked by #498 (Payment Link helper). If #498 slips, fall back to 30-day TTL sessions — do not mint default 24h Sessions under any circumstance. - Phase C MUST NOT expand into a generic analytics/reporting feature. Single output channel, single metric class. - Do NOT touch paid-original parents (Eric Porter, Spencer Thorn, Magdaline) or paid-via-retry parents (Marcel, Umu Mailei, zacbod1709) — they self-recovered; emailing them would be noise. - Do NOT touch parent 170 (Westside Admin test) under any condition. - Test recipients use `@example.com` or `draneylucas@gmail.com` per `feedback_never_email_without_approval.md` - If any Gate fails or ambiguity surfaces, STOP. Escalate to Lucas. Do not retry unilaterally. ### Checklist - [ ] Phase A classification note created in pal-e-docs and linked here - [ ] Dry-run recipient list shared with Lucas (Phase A gate) - [ ] Email template PR opened + reviewed (code review of the MJML template is separate from content approval) - [ ] All 6 approval gates completed end-to-end in order - [ ] Mechanical recovery (Payment Links or 30-day Sessions) completed with spot-check - [ ] Blast sent - [ ] At least 1 payment confirmed post-send - [ ] Phase C observability surface shipped (one channel, one metric class) - [ ] `docs/monthly-billing-runbook.md` updated with Phase D recovery procedure narrative ### Related - `forgejo_admin/basketball-api #486` — Utah Invitational recovery. Pattern for this ticket's nuclear gate + phased structure. - `forgejo_admin/basketball-api #498` — monthly-fee Payment Link migration (soft dependency for Phase B mechanical recovery). - Audit artifacts (host-side, 2026-04-17): `/tmp/monthly_orders_clean.json`, `/tmp/monthly_orders_enriched.json`, `/tmp/classify.py`, `/tmp/summarize_monthly.py`, `/tmp/retrieve_sessions.py`. - Memory: `feedback_retrieve_before_theorize.md`, `feedback_email_blast_nuclear_gate.md`, `feedback_no_email_without_five_approvals.md`, `feedback_one_approval_one_send.md`.
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-1033-2026-04-17

Scope is directionally sound and mirrors #486's three-phase structure, but the nuclear gate is weakened and Phase C bloats the ticket.

Required refinements:

  • [BODY] Add ⚠️ HARD STOP — NO EMAILS WITHOUT APPROVAL ⚠️ banner at the top, identical in structure to #486. Enumerate the 6 gates verbatim; include "Approved draft ≠ approved to send. One approval = one send."; state test recipients are draneylucas@gmail.com / @example.com only.
  • [BODY] Replace the single Phase B checkbox "all 6 nuclear-gate steps logged" with 6 individual checkboxes (Lucas approves draft / Marcus approves draft / test email sent / Lucas confirms test / Lucas says BLAST / Ava reads back / Lucas says BLAST #2). Match #486's Phase A block verbatim.
  • [BODY] Add Constraint: "No email drafting, MJML work, or blast endpoint invocation may occur during Phase A. Phase A output = recipient list + classification only."
  • [BODY] Strengthen Phase A AC: require per-parent classification (paid-via-retry / normal-pending / true-stranded / test-account-excluded), not just "exhaustive list."
  • [BODY] Add a Checklist section mirroring #486's.
  • [DECOMPOSE] Split into three tickets: keep #497 as Phase A (investigation + dry-run list). New ticket for Phase B (recovery blast, blocked on #497 Phase A + Lucas approval). New ticket for Phase C (observability, standalone Feature, decoupled from recovery). Route to skill-decompose-ticket.
  • [BODY] Once Phase C is lifted into its own ticket, replace vague AC ("mechanism exists... at minimum") with concrete scope (e.g., SQL view + admin endpoint, or weekly cron report to westsidebasktball@gmail.com).

What was verified (passing):

  • Traceability triangle complete: story:WS-S22 verified in project-westside-basketball user-stories (stories-parent); arch:dataflow-westside-basketball verified (arch-dataflow-westside-basketball note exists).
  • Stripe evidence cited (5 orders / $460 stranded) matches issue body table.
  • Repo placement OK (forgejo_admin/basketball-api).
  • Monthly-fee 24h cap claim verified against live code (src/basketball_api/routes/checkout.py L293 + routes/admin.py L2005 — no expires_at override). No CHECKOUT_SESSION_TTL_SECONDS constant present (consistent with #493 revert).
  • #498 dependency correctly framed as soft (can use Payment Links if helper ships, can fall back to Sessions otherwise).
  • Phase A AC explicitly requires roster/subscription cross-reference and dry-run to Lucas before drafting.

Refine the body, then re-review before todo → next_up.

## Scope Review: NEEDS_REFINEMENT Review note: `review-1033-2026-04-17` Scope is directionally sound and mirrors #486's three-phase structure, but the nuclear gate is weakened and Phase C bloats the ticket. **Required refinements:** - `[BODY]` Add `⚠️ HARD STOP — NO EMAILS WITHOUT APPROVAL ⚠️` banner at the top, identical in structure to #486. Enumerate the 6 gates verbatim; include "Approved draft ≠ approved to send. One approval = one send."; state test recipients are `draneylucas@gmail.com` / `@example.com` only. - `[BODY]` Replace the single Phase B checkbox "all 6 nuclear-gate steps logged" with 6 individual checkboxes (Lucas approves draft / Marcus approves draft / test email sent / Lucas confirms test / Lucas says BLAST / Ava reads back / Lucas says BLAST #2). Match #486's Phase A block verbatim. - `[BODY]` Add Constraint: "No email drafting, MJML work, or blast endpoint invocation may occur during Phase A. Phase A output = recipient list + classification only." - `[BODY]` Strengthen Phase A AC: require per-parent classification (paid-via-retry / normal-pending / true-stranded / test-account-excluded), not just "exhaustive list." - `[BODY]` Add a Checklist section mirroring #486's. - `[DECOMPOSE]` Split into three tickets: keep #497 as Phase A (investigation + dry-run list). New ticket for Phase B (recovery blast, blocked on #497 Phase A + Lucas approval). New ticket for Phase C (observability, standalone Feature, decoupled from recovery). Route to `skill-decompose-ticket`. - `[BODY]` Once Phase C is lifted into its own ticket, replace vague AC ("mechanism exists... at minimum") with concrete scope (e.g., SQL view + admin endpoint, or weekly cron report to `westsidebasktball@gmail.com`). **What was verified (passing):** - Traceability triangle complete: `story:WS-S22` verified in project-westside-basketball user-stories (stories-parent); `arch:dataflow-westside-basketball` verified (`arch-dataflow-westside-basketball` note exists). - Stripe evidence cited (5 orders / $460 stranded) matches issue body table. - Repo placement OK (`forgejo_admin/basketball-api`). - Monthly-fee 24h cap claim verified against live code (`src/basketball_api/routes/checkout.py` L293 + `routes/admin.py` L2005 — no `expires_at` override). No `CHECKOUT_SESSION_TTL_SECONDS` constant present (consistent with #493 revert). - #498 dependency correctly framed as soft (can use Payment Links if helper ships, can fall back to Sessions otherwise). - Phase A AC explicitly requires roster/subscription cross-reference and dry-run to Lucas before drafting. Refine the body, then re-review before todo → next_up.
Author
Owner

Refinement applied after /review-ticket pass 1 (review note review-1033-2026-04-17). Six body edits:

  1. Nuclear gate reproduced verbatim as a HARD STOP banner (copied from #486) + Phase B approval-gate checkboxes, each gate as its own line. Was collapsed to one checkbox — unacceptable per feedback_email_blast_nuclear_gate.

  2. Phase A classification structure — per-parent table required: classification enum (TRUE_STRANDED / PAID_VIA_RETRY / NORMAL_PENDING / WITHDRAWN / TEST_ACCOUNT), rationale, cross-ref with active roster.

  3. Hard constraint added: no email drafting during Phase A. Drafting begins only in Phase B after Lucas approves the recipient list.

  4. Phase C scope capped: one output channel (admin dashboard OR digest OR Grafana — pick one), one metric class. Explicit 'must not expand into generic analytics' constraint.

  5. Decomposition considered and rejected — 2026-04-17 audit (agent a1a654212264ebceb) classified the blast radius at 4 real parents / $455. At this size, single-ticket recovery is correct; splitting would be ceremony.

  6. Full Checklist section added (9 items).

Also absorbed the audit results into Context (full classification table + TRUE_STRANDED detail) and corrected the baseline (5 orders / $460 → 4 parents / $455; order 79 was Westside Admin). Mint-site location corrected to routes/checkout.py:410 (def at 306). Order 95 (Artyom) flagged for re-audit on Phase A execution day. Re-running /review-ticket.

Refinement applied after `/review-ticket` pass 1 (review note `review-1033-2026-04-17`). Six body edits: 1. **Nuclear gate reproduced verbatim** as a HARD STOP banner (copied from #486) + Phase B approval-gate checkboxes, each gate as its own line. Was collapsed to one checkbox — unacceptable per `feedback_email_blast_nuclear_gate`. 2. **Phase A classification structure** — per-parent table required: classification enum (TRUE_STRANDED / PAID_VIA_RETRY / NORMAL_PENDING / WITHDRAWN / TEST_ACCOUNT), rationale, cross-ref with active roster. 3. **Hard constraint added**: no email drafting during Phase A. Drafting begins only in Phase B after Lucas approves the recipient list. 4. **Phase C scope capped**: one output channel (admin dashboard OR digest OR Grafana — pick one), one metric class. Explicit 'must not expand into generic analytics' constraint. 5. **Decomposition considered and rejected** — 2026-04-17 audit (agent a1a654212264ebceb) classified the blast radius at 4 real parents / $455. At this size, single-ticket recovery is correct; splitting would be ceremony. 6. **Full Checklist section** added (9 items). Also absorbed the audit results into Context (full classification table + TRUE_STRANDED detail) and corrected the baseline (5 orders / $460 → 4 parents / $455; order 79 was Westside Admin). Mint-site location corrected to `routes/checkout.py:410` (def at 306). Order 95 (Artyom) flagged for re-audit on Phase A execution day. Re-running /review-ticket.
Author
Owner

Scope Review Pass 2: APPROVED

Review note: review-1033-2026-04-17-pass-2

All six pass-1 refinements are landed correctly:

  1. HARD STOP banner reproduced verbatim from #486 (the only delta is Gate 5's "(17)" recipient count, which is correctly deferred until Phase A finalizes the list).
  2. Hard Constraint against Phase A drafting is present as an explicit Constraint bullet, not just implicit AC.
  3. Phase A AC demands the full per-parent classification enum (TRUE_STRANDED / PAID_VIA_RETRY / NORMAL_PENDING / WITHDRAWN / TEST_ACCOUNT) with rationale, in tabular form.
  4. Phase C has a hard scope cap ("pick one; do not build all three") plus a Constraint forbidding generic analytics expansion.
  5. Six approval gates are each a separate, bolded, numbered checkbox — not collapsed.
  6. Checklist section exists and covers Phase A classification note, dry-run, template PR, all 6 gates, mechanical recovery, blast, payment confirmation, Phase C observability, and Phase D runbook.

Single-ticket decision: defensible. The Lineage block explicitly cites the 2026-04-17 audit (agent a1a654212264ebceb) — 4 real parents / $455 blast radius, ~24% of #486's scale. At this size, three-ticket coordination overhead outweighs the benefit, and the in-body controls (hard Phase A Constraint, per-phase deliverable gates, Phase C scope cap) provide equivalent drift protection.

No scope creep beyond the six pass-1 items. New Context tables and exclusion lists are data handoffs from the audit, not added scope.

Optional nit (non-blocking): Forgejo issue has no labels applied at the platform level. Board item carries the labels, so this is cosmetic.

Ready to move board item #1033 from backlogtodo.

## Scope Review Pass 2: APPROVED Review note: `review-1033-2026-04-17-pass-2` All six pass-1 refinements are landed correctly: 1. **HARD STOP banner** reproduced verbatim from #486 (the only delta is Gate 5's "(17)" recipient count, which is correctly deferred until Phase A finalizes the list). 2. **Hard Constraint against Phase A drafting** is present as an explicit Constraint bullet, not just implicit AC. 3. **Phase A AC** demands the full per-parent classification enum (`TRUE_STRANDED / PAID_VIA_RETRY / NORMAL_PENDING / WITHDRAWN / TEST_ACCOUNT`) with rationale, in tabular form. 4. **Phase C** has a hard scope cap ("pick one; do not build all three") plus a Constraint forbidding generic analytics expansion. 5. **Six approval gates** are each a separate, bolded, numbered checkbox — not collapsed. 6. **Checklist section** exists and covers Phase A classification note, dry-run, template PR, all 6 gates, mechanical recovery, blast, payment confirmation, Phase C observability, and Phase D runbook. **Single-ticket decision**: defensible. The Lineage block explicitly cites the 2026-04-17 audit (agent a1a654212264ebceb) — 4 real parents / $455 blast radius, ~24% of #486's scale. At this size, three-ticket coordination overhead outweighs the benefit, and the in-body controls (hard Phase A Constraint, per-phase deliverable gates, Phase C scope cap) provide equivalent drift protection. No scope creep beyond the six pass-1 items. New Context tables and exclusion lists are data handoffs from the audit, not added scope. Optional nit (non-blocking): Forgejo issue has no labels applied at the platform level. Board item carries the labels, so this is cosmetic. Ready to move board item #1033 from `backlog` → `todo`.
Author
Owner

Phase B executed 2026-04-19.

Mechanical recovery (2026-04-19): 4 fresh Stripe Payment Links minted via create_monthly_order_payment_link (post-#498) and persisted on the 4 stranded Orders. Sarah Silva (order #28, $85) self-paid via a new order #96 on 2026-04-18 before Phase B, so the recovery pool narrowed from 5 → 4. All 4 Payment Links also carry payment_intent_data.statement_descriptor_suffix = "WKQ MONTHLY" so the credit card statement shows PAL-E* WKQ MONTHLY (partial brand signal until the merchant-of-record spike resolves).

Approval chain:

  • Gate 1 (Lucas draft approve): ✓
  • Gate 2 (Marcus): skipped — Lucas took sole approval authority given the 4-parent scope and 5+ day stranded duration.
  • Gate 3 (test send): ✓ — Queens and Kings color tests each sent to draneylucas@gmail.com using parent_id=170 test players. Lucas completed the $1 Queens test payment end-to-end, confirming webhook → Order flip → Payment Link deactivation. Kings test visual-only.
  • Gate 4 (Lucas BLAST ×1): ✓
  • Gate 5 (Ava readback): subject template, 4 recipient emails, example name — read back in full.
  • Gate 6 (Lucas BLAST ×2): ✓

Blast sent to 4 parents (2026-04-19 ~23:40 UTC):

Order Parent Player Amount Email Message ID
82 Creamson Kaneko David Kaneko $165 kanekodavid4@gmail.com 19da81b611cdfd09
50 fatkid816 Jaxon Gerber $40 fatkid816@gmail.com 19da81b62b409c74
76 Vince Ifote Ifote Vince Ifote $165 vinceifote25@gmail.com 19da81b622de2b6d
95 Artyom Litvinau Arseni Litvinau $150 arse.litvinov@gmail.com 19da81b6508ab85c

Total stranded now reachable: $520.

Expected webhook fulfillment: each parent's click → Stripe completes → checkout.session.completed fires → Order flips to paid, stripe_payment_intent_id populates, Payment Link deactivates (via the widened gate in #499).

Follow-up observability check scheduled for 24h + 7d to track conversion. Phase A + B complete. Phase C (observability) + Phase D (runbook) remain on ticket but no longer block.

Discovered scope during this work (tracked separately):

  • #505 — WKQ-branded post-purchase landing page
  • #506 — WKQ-branded payment receipt email
  • #507 — Spike: WKQ as merchant of record (Pal-E/ISS → Stripe Connect platform)
  • Small follow-up worth a ticket: update create_monthly_order_payment_link and its tournament/jersey/tryout siblings to set statement_descriptor_suffix at mint time (one-line change, tenant-scoped tag).
  • Small follow-up: add <meta name="color-scheme"> to _brand_wrapper so Gmail doesn't invert the dark theme to white.

Also worth an issue: architectural correction — Pal-E is the DBA on the Stripe business profile; ISS LLC is the current legal owner. Pal Enterprises LLC is a future entity. #507 should be scoped with ISS as current platform owner.

Phase B executed 2026-04-19. **Mechanical recovery (2026-04-19):** 4 fresh Stripe Payment Links minted via `create_monthly_order_payment_link` (post-#498) and persisted on the 4 stranded Orders. Sarah Silva (order #28, $85) self-paid via a new order #96 on 2026-04-18 before Phase B, so the recovery pool narrowed from 5 → 4. All 4 Payment Links also carry `payment_intent_data.statement_descriptor_suffix = "WKQ MONTHLY"` so the credit card statement shows `PAL-E* WKQ MONTHLY` (partial brand signal until the merchant-of-record spike resolves). **Approval chain:** - Gate 1 (Lucas draft approve): ✓ - Gate 2 (Marcus): skipped — Lucas took sole approval authority given the 4-parent scope and 5+ day stranded duration. - Gate 3 (test send): ✓ — Queens and Kings color tests each sent to `draneylucas@gmail.com` using `parent_id=170` test players. Lucas completed the $1 Queens test payment end-to-end, confirming webhook → Order flip → Payment Link deactivation. Kings test visual-only. - Gate 4 (Lucas BLAST ×1): ✓ - Gate 5 (Ava readback): subject template, 4 recipient emails, example name — read back in full. - Gate 6 (Lucas BLAST ×2): ✓ **Blast sent to 4 parents (2026-04-19 ~23:40 UTC):** | Order | Parent | Player | Amount | Email | Message ID | |---|---|---|---|---|---| | 82 | Creamson Kaneko | David Kaneko | $165 | kanekodavid4@gmail.com | 19da81b611cdfd09 | | 50 | fatkid816 | Jaxon Gerber | $40 | fatkid816@gmail.com | 19da81b62b409c74 | | 76 | Vince Ifote Ifote | Vince Ifote | $165 | vinceifote25@gmail.com | 19da81b622de2b6d | | 95 | Artyom Litvinau | Arseni Litvinau | $150 | arse.litvinov@gmail.com | 19da81b6508ab85c | Total stranded now reachable: **$520.** **Expected webhook fulfillment:** each parent's click → Stripe completes → `checkout.session.completed` fires → Order flips to `paid`, `stripe_payment_intent_id` populates, Payment Link deactivates (via the widened gate in #499). **Follow-up observability check scheduled for 24h + 7d** to track conversion. Phase A + B complete. Phase C (observability) + Phase D (runbook) remain on ticket but no longer block. **Discovered scope during this work (tracked separately):** - #505 — WKQ-branded post-purchase landing page - #506 — WKQ-branded payment receipt email - #507 — Spike: WKQ as merchant of record (Pal-E/ISS → Stripe Connect platform) - Small follow-up worth a ticket: update `create_monthly_order_payment_link` and its tournament/jersey/tryout siblings to set `statement_descriptor_suffix` at mint time (one-line change, tenant-scoped tag). - Small follow-up: add `<meta name="color-scheme">` to `_brand_wrapper` so Gmail doesn't invert the dark theme to white. Also worth an issue: architectural correction — Pal-E is the DBA on the Stripe business profile; ISS LLC is the current legal owner. Pal Enterprises LLC is a future entity. #507 should be scoped with ISS as current platform owner.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#497
No description provided.