Add player_ids filter to query_unsigned_contracts + blast endpoint #448
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#448
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 — discovered during 2026-04-10 blast incident (basketball-api#445). The
query_unsigned_contractsquery inemail_queries.pyhas only atest_emailkwarg for filtering. There is no way to target a specific subset of players or exclude specific players from a blast without NULLing their contract_tokens directly (destructive, requires undo to restore).Repo
forgejo_admin/basketball-apiUser Story
As Ava (or any admin) running a blast
I want to specify exactly which players to include or exclude from a contract blast
So that I can send to "these 15 players" without over-sending or having to NULL tokens on players I want to exclude
Context
Current
query_unsigned_contractssignature (src/basketball_api/services/email_queries.py:31):The only filter is
test_email, which is a post-query Python-level string compare (if parent.email != test_email: continue). Noplayer_ids,exclude_player_ids, orparent_idsparams.The
/admin/email/blastendpoint passesbody.test_emailthrough to the query function but doesn't accept any other filter params for the recipient set.What this cost tonight: the 2026-04-10 blast intended for 15 players sent to 18 because the 3 Apaisa sisters had active contract_tokens and shared one parent email. Root cause in basketball-api#445.
File Targets
Files to modify:
src/basketball_api/services/email_queries.py— addplayer_ids: list[int] | None = Noneandexclude_player_ids: list[int] | None = Nonekwargs toquery_unsigned_contracts(and optionallyquery_incomplete_profilesfor consistency)src/basketball_api/routes/admin.py— addplayer_idsandexclude_player_idsoptional fields toBlastRequestPydantic model, pass them through to the query functiontests/test_email_queries.py— new tests for both include and exclude semanticstests/test_admin_email_blast.py— new integration test verifying blast endpoint honors the filtersAcceptance Criteria
query_unsigned_contracts(db, tenant, player_ids=[97, 202])returns only those 2 player rows (if they match the other filters)query_unsigned_contracts(db, tenant, exclude_player_ids=[111, 186, 112])returns all offered players EXCEPT those 3test_email— if all three are provided, the intersection appliesPOST /admin/email/blastaccepts optionalplayer_idsandexclude_player_idsarrays in the request bodyplayer_idsandexclude_player_idsare empty/None, current behavior is preserved (backward-compatible)Test Expectations
test_query_unsigned_contracts_player_ids_includetest_query_unsigned_contracts_player_ids_excludetest_query_unsigned_contracts_combined_filterstest_blast_endpoint_honors_player_idsConstraints
test_emailbehavior.filter(Player.id.in_(player_ids))and.filter(~Player.id.in_(exclude_player_ids))— do NOT filter in Python post-query (that's the slow, error-prone pattern)query_unsigned_contracts— psycopg + SQLAlchemy, no new dependenciesemail_queries.py::QUERY_REGISTRYChecklist
player_ids=[97, 202]returns exactly 2 recipients via test_email flowexclude_player_ids=[111, 186, 112]excludes Apaisa sistersRelated
westside-basketball— project this affectsfeedback_blast_pool_verification.md— companion lesson