Incident: 3 wrong contract emails sent to Sandra Apaisa (2026-04-10 blast) #445
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
ldraney/basketball-api#445
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
Incident — 2026-04-10 23:50 UTC during the Marcus 2026-04-10 contract batch blast.
Repo
forgejo_admin/basketball-apiWhat Broke
During the Marcus-approved 15-player contract blast, 18 emails were sent instead of 15. The 3 extras went to
apaisasandra@gmail.comat the OLD rates (Aleiyah $59, Analeigh $58, Ayvah $58) — NOT the new $50/girl family rate Marcus and Lucas had agreed on for the Apaisa sisters.All 3 Apaisa contract_tokens were immediately NULLed after the mistake was detected, so if Sandra clicks any of the 3 links, she gets a 404. The emails are in her inbox and visible, but the contracts cannot be signed at the wrong rate.
email_log rows: 548, 549, 550 — all
email_type=contract_offer, all toapaisasandra@gmail.com, all sent 2026-04-10 23:50:36-37 UTC, all with real gmail_message_ids (delivered).Notable side bug: the 3 email_log rows have NULL player_id. The
query_unsigned_contractsquery returned the recipient data but the/admin/email/blastendpoint didn't store player_id back into email_log. Separate logging bug — file as follow-up if not already tracked.Repro Steps
POST /admin/email/blastwithquery=unsigned_contracts, notest_emailfilterplayerswherecontract_status='offered' AND contract_token IS NOT NULL, scoped by tenant — with NO way to exclude specific players without NULLing their tokens firstExpected Behavior
The blast endpoint should have either:
player_ids: list[int]optional param onquery_unsigned_contractsso callers can target specific players, ORexclude_player_idsparam for the inverse, ORparent_idsparam for groupingNone of these exist today. The workarounds available tonight were:
test_email=Xwhich filters to one recipient (used for single-player sends but not for "everyone except these")Environment
query_unsigned_contractsat src/basketball_api/services/email_queries.py:31)apaisasandra@gmail.com(Sandra Apaisa, parent of 3 Queens girls)Root Cause
Two compounding failures by the operator (Ava):
Verification query ≠ endpoint query. Lucas asked "is the blast pool 15 players?" Ava ran
SELECT ... FROM players WHERE contract_status = 'offered' AND contract_token IS NOT NULL AND name NOT ILIKE '%Apaisa%'and returned count 15. But the actualquery_unsigned_contractsendpoint query has NO name filter. Ava verified against an invented query instead of the one that would actually run.Missed Apaisa token NULL step. Earlier in the session, Ava proposed NULLing Analeigh and Ayvah's contract_tokens as part of the Apaisa consolidation plan. Lucas asked "wait why?" and Ava moved on without executing it. The tokens stayed live. When the blast fired, all 3 Apaisa rows matched the endpoint query.
Either failure alone would have been caught by the other — a correct verification would have shown 18 rows in the pool, OR the NULL tokens would have dropped the 3 extras.
Impact
$58or$59/monthin the contract page header — the OLD rates, not the $50 family rate_brand_wrapper()(the right architecture for Queens branding), sent test to Lucas, then sent REVIEW copy to Marcus in his Gmail + GroupMe heads-up. Pending Marcus's thumbs-up before sending the real correction to Sandra.Acceptance Criteria
fjOXPKtm..., Analeigh id 186 tokenWqqJGPbu..., Ayvah id 112 tokenQLHbMP5K...)player_idsparam on blast endpoint, (b) NULL player_id in email_log bug, (c)unsigned_contractsverification SOPfeedback_blast_pool_verification.mdmemory file saved (lesson: run the EXACT endpoint query before any blast)Related
westside-basketball— project this affectsfeedback_email_blast_nuclear_gate.md(prior blast gate discipline, insufficient against this failure mode)feedback_blast_gate_failure.md(prior incident, same class)